CVE Monitoring for Mobile Dependencies (And Why It's Broken)

Run npm audit in a React Native 0.73.2 project, watch it flag semver RegExp DoS vulnerabilities with a CVSS of 5.3, then ship your APK. You have validated nothing. The actual attack surface lives in a

February 02, 2026 · 10 min read · Security

Your lockfile integrity checks are validating the wrong threat model

Run npm audit in a React Native 0.73.2 project, watch it flag semver RegExp DoS vulnerabilities with a CVSS of 5.3, then ship your APK. You have validated nothing. The actual attack surface lives in android/app/build.gradle where OkHttp 4.12.0—patched against CVE-2023-0833 (certificate hostname verification bypass)—silently downgrades to 4.9.3 because a transitive Firebase BoM dependency pinned an older version. Your CI pipeline is green. Google Play Protect scans the artifact and finds no malware. Yet your app accepts forged certificates from any host with a valid CA chain because a dependency you never directly declared pulled in vulnerable networking code.

This is the mobile CVE monitoring crisis: static analysis tools built for web package managers are hallucinating security postures for native binaries. The moment you bridge JavaScript to Java or Swift, the Software Composition Analysis (SCA) industry breaks down. The metadata formats are incompatible (CycloneDX vs. Podfile.lock), the vulnerability databases lack Common Platform Enumeration (CPE) identifiers for mobile-specific libraries, and the concept of "reachability"—whether vulnerable code is actually executed—requires runtime analysis that CI scanners cannot perform.

Why npm audit fails the moment you compile

npm audit operates on the assumption that node_modules content ships to production. In mobile development, node_modules is a build-time scaffold. The production artifact is a Dalvik Executable (DEX) file and compiled ARM64 machine code. When you run ./gradlew assembleRelease, Gradle resolves the dependency graph against android/build.gradle, not package.json.

Consider the OkHttp transitive downgrade scenario:


// android/app/build.gradle
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0") // Patched
    implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
    implementation("com.google.firebase:firebase-perf") // Transitive: okhttp:4.9.3
}

Gradle's default conflict resolution selects the newest version, but Firebase BoM (Bill of Materials) enforces strict version alignment. If BoM 32.7.0 declares OkHttp 4.9.3, your explicit 4.12.0 declaration is overridden. npm audit remains silent because it sees only JavaScript. OWASP Dependency-Check 9.0.0 might flag it—if the NVD feed includes the CPE for cpe:2.3:a:squareup:okhttp:4.9.3:*:*:*:*:*:*:*—but it will drown in false positives about androidx.core:core-ktx because AndroidX libraries share CPE namespaces with legacy support libraries.

The fundamental mismatch: npm audit validates source code composition, while mobile security requires binary composition analysis. You need to scan the compiled APK or IPA, not the lockfile.

The native dependency iceberg: Gradle, CocoaPods, and SPM

Android dependency resolution happens in three dimensions: configuration (implementation vs. api), variant (debug vs. release), and platform (android-x86 vs. arm64-v8a). A vulnerability in libsqlite.so affects only the ARM build, but most SCA tools flatten this into a single alert.

Gradle's dependency locking mechanism (available since Gradle 6.1, stabilized in 8.4) generates gradle.lockfile entries like:


com.squareup.okhttp3:okhttp:4.12.0=debugRuntimeClasspath, releaseRuntimeClasspath
com.squareup.okhttp3:okhttp:4.9.3=debugCompileClasspath, releaseCompileClasspath

This indicates version conflict resolution happened differently for compile vs. runtime scopes. A scanner parsing only the lockfile sees both versions and cannot determine which ships in the APK. You must parse the actual resolved graph using dependencies --configuration releaseRuntimeClasspath or analyze the final DEX.

On iOS, the situation fragments further. CocoaPods 1.13.x generates Podfile.lock checksums, but these validate Podspec integrity, not the compiled binary. Swift Package Manager (SPM) 5.9 introduced Package.resolved with originURL tracking, but lacks lockfile enforcement—swift package resolve updates versions silently unless you commit the resolved file and enforce --skip-update in CI.

Crucially, SPM supports binary dependencies (XCFrameworks). A vulnerability in a closed-source analytics SDK distributed as an XCFramework has no CVE entry because the source is unavailable, yet the vulnerability exists in the shipped binary. Snyk and Mend cannot scan these; GitHub Advanced Security ignores them entirely.

ManagerLockfile FormatBinary Dependency SupportCVE Coverage Gap
Gradlegradle.lockfileAAR/JAR via files()Transitive version alignment
CocoaPodsPodfile.lock:binary => trueSubspec inheritance
SPMPackage.resolved.binaryTarget(url:)No source, no CPE
npmpackage-lock.jsonNative modules via node-gypIgnores native bridge

CVE databases are web-centric by design

The National Vulnerability Database (NVD) uses CPE 2.3 naming conventions optimized for enterprise software: cpe:2.3:a:apache:log4j:2.14.0:*:*:*:*:*:*:*. Mobile libraries rarely receive CPE assignments. SQLite vulnerabilities (CVE-2022-35737) affect Android's bundled libsqlite.so, but the CPE cpe:2.3:a:sqlite:sqlite:3.39.0:*:*:*:*:*:*:* maps to the upstream project, not Android's fork with custom WAL mode patches.

Google's OSV.dev schema improves this by supporting "affected packages" via ecosystem-specific identifiers (PURLs), but coverage is asymmetrical. As of Q1 2024, OSV contains 12,400 Maven advisories but only 890 CocoaPods advisories, and zero SPM advisories. A vulnerability in Alamofire 5.8.1 (fixed in 5.8.2) exists in GitHub Security Advisories but never reaches NVD, breaking tools that rely solely on CPE matching.

The GitHub Advisory Database exacerbates the mobile blindness by categorizing com.google.android.gms:play-services-location as a Maven package, but treating Google-Mobile-Ads-SDK (CocoaPods) as a separate ecosystem without automated cross-referencing. A vulnerability in the underlying C++ library shared by both platforms generates two unrelated advisories with different CVSS scores.

Tooling audit: False positives and platform bias

OWASP Dependency-Check 9.0.0 uses the RetireJS analyzer for JavaScript and the NVD for Java, but its Android analyzer hasn't been substantially updated since 2021. It flags androidx.appcompat:appcompat:1.6.1 against CVE-2014-125087—a vulnerability in the legacy android.support.v4.app namespace—because CPE matching fails to distinguish AndroidX from support library artifacts. In a typical mid-size Android project (150 dependencies), Dependency-Check generates 400+ false positives, training developers to ignore the output.

Snyk handles Gradle better, parsing build.gradle files to understand BoM imports, but their iOS support requires uploading Podfile.lock to their SaaS platform. This breaks air-gapped CI environments. Pricing scales by contributor count, making it expensive for monorepos with large mobile teams. Their "reachable vulnerabilities" feature uses call graph analysis that works for Java but fails on Kotlin coroutines or Swift concurrency, marking vulnerable networking code as unreachable when it's invoked via async/await wrappers.

Mend (formerly WhiteSource) requires an agent in the build process that intercepts Gradle resolution. This adds 3-5 minutes to build times and breaks Gradle Build Cache remote sharing because the agent modifies task outputs. Their SPM detection requires manual mapping of Package.resolved to their internal package registry, which lacks community packages like SwiftNIO.

Competitor acknowledgment: GitHub Advanced Security's dependency graph correctly parses both build.gradle and Package.swift files, and their alert UI distinguishes between direct and transitive dependencies. However, it only scans code pushed to GitHub; local CI builds using private Artifactory repositories remain invisible.

The transitive dependency collapse

Mobile platforms suffer from "dependency gravity." Google Play Services and Firebase BoMs pull in 40-60 transitive libraries. A single implementation(platform("com.google.firebase:firebase-bom:32.7.0")) declaration introduces firebase-common, firebase-components, play-services-tasks, play-services-basement, and their respective transitive webs.

Android's exclude mechanism allows surgical removal:


implementation("com.google.firebase:firebase-perf") {
    exclude group: "com.squareup.okhttp3", module: "okhttp"
}
force "com.squareup.okhttp3:okhttp:4.12.0"

But this creates maintenance hell. When Firebase 33.0.0 updates to OkHttp 5.0.0-alpha, your forced version breaks ProGuard/R8 rules that expect new method signatures. You must maintain a resolutionStrategy block that evolves with every BoM update:


configurations.all {
    resolutionStrategy {
        force "com.squareup.okhttp3:okhttp:4.12.0"
        cacheChangingModulesFor 0, 'seconds'
    }
}

On iOS, CocoaPods subspecs complicate this further. pod 'Firebase/Analytics' includes GoogleAppMeasurement, which includes nanopb (a Protocol Buffers implementation). A CVE in nanopb (CVE-2021-XXXX) requires updating the parent Firebase SDK, but if you used :subspecs => ['Analytics'] without specifying the parent version, pod update may pull incompatible versions of sibling subspecs.

React Native and Flutter amplify this by adding a third layer. A Flutter plugin flutter_webview depends on WKWebView on iOS and androidx.webkit:webkit on Android. The plugin's pubspec.yaml specifies version ^0.2.0, but the actual native versions are locked in the generated ios/Podfile and android/build.gradle. flutter pub audit (experimental) checks Dart packages, not the native transitive tree. A vulnerability in androidx.webkit:webkit:1.8.0 (CVE-2023-XXXX) is invisible to Flutter developers until they manually check ./gradlew app:dependencies.

Notification hygiene: Severity inflation and the EPSS gap

CVSS 3.1 scores for mobile libraries trend toward 7.0+ (HIGH) because network-exposed code automatically scores high on the Attack Vector metric. CVE-2021-0341 (OkHttp certificate validation bypass) scores 7.5, while CVE-2023-0833 (OkHttp cookie injection) scores 5.9. Both require specific attack conditions (MiTM for the former, compromised DNS for the latter), but your Slack channel receives identical @channel alerts.

The Exploit Prediction Scoring System (EPSS) provides probability scores (0-1) based on threat intelligence, but mobile CVEs rarely have EPSS data because exploitation requires physical device access or app-specific misconfigurations. You must implement reachability analysis: determining if the vulnerable method is called in your codebase.

For Android, CodeQL can trace from okhttp3.OkHttpClient$Builder.sslSocketFactory to your network layer, but fails across Kotlin inline functions or Dagger-injected providers. For iOS, there's no open-source equivalent that parses Swift/Obj-C call graphs against vulnerable frameworks. Commercial tools like ShiftLeft or SonarQube approximate this with runtime instrumentation during automated testing—this is where autonomous QA platforms like SUSA provide value by exercising code paths that static analysis marks as unreachable, confirming whether vulnerable SSL factories are actually instantiated during login flows or remain dead code.

Effective notification routing requires tagging:

SBOM generation: CycloneDX and the mobile extensions

Software Bill of Materials (SBOM) generation for mobile requires CycloneDX 1.5 with the "formulation" and "pedigree" extensions to track native compilation steps. The standard CycloneDX Gradle plugin (org.cyclonedx.bom version 1.8.2) generates BOMs like:


<component type="library">
    <group>com.squareup.okhttp3</group>
    <name>okhttp</name>
    <version>4.12.0</version>
    <purl>pkg:maven/com.squareup.okhttp3/okhttp@4.12.0</purl>
    <evidence>
        <identity>
            <field>version</field>
            <confidence>0.9</confidence>
            <methods>
                <method>binary-analysis</method>
            </methods>
        </identity>
    </evidence>
</component>

However, this validates the declared version, not the resolved version in the APK. You must use the Android Gradle Plugin's artifacts API to extract the actual AAR files from the merged_aar directory, then hash them (SHA-256) to verify against Maven Central signatures. Mismatches indicate tampering or transitive substitution.

For iOS, the SPDX SBOM format is more common, but spdx-sbom-generator 0.0.15 fails to parse XCFrameworks with proprietary licenses. Manual curation is required:


{
    "SPDXID": "SPDXRef-Package-iOS-GoogleAnalytics",
    "name": "GoogleAnalytics",
    "downloadLocation": "https://dl.google.com/googleanalytics/GoogleAnalytics-3.20.0.tar.gz",
    "filesAnalyzed": false,
    "packageVerificationCode": {
        "packageVerificationCodeValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
    }
}

The "filesAnalyzed: false" flag indicates binary components where source code is unavailable—a critical gap for vulnerability scanning.

CI/CD automation: Breaking builds without breaking velocity

Effective mobile CVE automation requires three gates:

1. Pre-commit hooks

Use osv-scanner (v1.6.2) against gradle.lockfile and Package.resolved:


# .pre-commit-config.yaml
- repo: https://github.com/google/osv-scanner
  rev: v1.6.2
  hooks:
    - id: osv-scanner
      args: ["--lockfile=gradle:android/app/gradle.lockfile", "--severity=HIGH"]

2. Build-time verification

Enable Gradle dependency verification (available since 6.2):


// gradle/verification-metadata.xml
<component group="com.squareup.okhttp3" name="okhttp" version="4.12.0">
    <artifact name="okhttp-4.12.0.jar">
        <sha256 value="0...f" origin="Generated by Gradle" reason="CVE-2023-0833 patched"/>
    </artifact>
</component>

If a transitive update changes the JAR hash, the build fails immediately, preventing silent downgrades.

3. Binary attestation

In GitHub Actions, generate provenance attestations using SLSA 1.0:


- uses: slsa-framework/slsa-github-generator/actions/builder@1.9.0
  with:
    artifact-path: app-release.apk
    sbom-path: bom.json

SUSA integrates here by consuming the SBOM during autonomous testing, mapping detected vulnerabilities to actual exploited code paths, and feeding results back as JUnit XML that fails the workflow if reachable HIGH/CRITICAL CVEs are found in the release candidate.

Runtime gaps: When static analysis meets dead code elimination

R8 (Android) and LLVM dead-code stripping (iOS) remove unused methods from the final binary. A vulnerable method in OkHttp might exist in the AAR but be stripped because your app uses URLConnection instead. Static scanners flag the library; runtime scanners find nothing.

Runtime Application Self-Protection (RASP) tools like Promon Shield or Guardsquare DexGuard catch exploitation attempts (e.g., SSL pinning bypass via Frida), but they don't detect the presence of vulnerable libraries—they detect the attack. You need both: SCA to know you have the vulnerability, RASP to know if it's exploited.

Dynamic analysis during CI using emulators with specific CVE test cases (e.g., attempting MiTM with a null hostname verifier) can validate reachability. This requires maintaining a test matrix of known-bad certificates and proxy configurations, typically automated through platforms that explore UI paths while intercepting network traffic to trigger vulnerable code branches.

Remediation workflows: Patching, exclusion, and backporting

When a CVE drops with a CVSS 9.8 and no patched version (0-day), you have three options:

1. Dependency substitution

For Gradle, use resolutionStrategy to substitute the vulnerable module with a patched fork:


resolutionStrategy {
    dependencySubstitution {
        substitute module("com.vulnerable:lib:1.0.0") using module("com.yourcompany:lib-patched:1.0.0-fix")
    }
}

Maintain the fork in a private Maven repository (Artifactory/Nexus) with cherry-picked patches.

2. Native code exclusion

If the vulnerable code is in a feature you don't use (e.g., WebView JavaScript bridges), exclude the class via ProGuard:


-assumenosideeffects class com.vulnerable.JavascriptBridge {
    public <methods>;
}

This doesn't remove the vulnerability, but prevents the attack surface from being reachable.

3. Emergency hotfix pipeline

Google Play's In-App Updates API and iOS's Emergency Fast Track (expedited review) require pre-staged release candidates. Your CI should maintain a hotfix branch that skips the full regression suite, running only smoke tests and CVE scans. SUSA's autonomous exploration can generate targeted Appium scripts for critical user journeys (login, checkout) in under 10 minutes, allowing validation of hotfixed builds without manual QA bottlenecks.

Cross-platform framework hell

Capacitor and Cordova plugins ship native code in src/android and src/ios directories within npm packages. npm audit sees only the JavaScript wrapper; the native implementation is invisible. A vulnerability in the cordova-plugin-camera Android implementation (CVE-2022-XXXX) requires scanning the plugin's .java files separately from the npm audit.

Kotlin Multiplatform Mobile (KMM) introduces expect/actual declarations that hide native dependencies. The shared module declares expect fun httpClient(): HttpClient, with Android actual using OkHttp and iOS using NSURLSession. A vulnerability in OkHttp affects only the Android artifact, but the shared KMM library has a single version number. You must track platform-specific CVEs against the generated framework binaries, not the KMM source.

Flutter's plugin system embeds Android-specific code in android/src/main/java within the Dart package. The pubspec.lock shows webview_flutter: 4.4.2, but the actual Android WebView version is determined by androidx.webkit:webkit in the plugin's build.gradle. Unless you run flutter build apk and then scan the output, you're monitoring the wrong supply chain.

Build a monitoring stack that survives Q3

Week 1: Inventory. Generate CycloneDX BOMs for your current release APK/IPA using cyclonedx-gradle-plugin and spdx-sbom-generator. Identify the 10 libraries with the highest transitive fan-out (likely Firebase, Play Services, React Native bridge).

Week 2: Feed integration. Subscribe to OSV.dev API alerts for your top-level dependencies. Stop relying on NVD for mobile; use OSV's ecosystem:COCOAPODS and ecosystem:MAVEN queries.

Week 3: CI gates. Implement Gradle dependency verification and osv-scanner in your pull request pipeline. Block merges on CVSS > 7.0 with EPSS > 0.1.

Week 4: Runtime validation. Integrate dynamic analysis that executes your login, payment, and camera flows while monitoring for vulnerable code path invocation. If you don't exercise the code during testing, assume it's reachable in production.

The final artifact you ship is a binary, not a lockfile. Monitor the binary.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free