home intel cve-2026-0035-mediaprovider-createrequest-privilege-escalation
CVE Analysis 2026-03-02 · 8 min read

CVE-2026-0035: MediaProvider createRequest Logic Error Enables Privilege Escalation

A logic error in MediaProvider.java's createRequest grants arbitrary read/write to non-existent files, enabling local privilege escalation with no user interaction required.

#path-traversal#privilege-escalation#media-provider#file-access#logic-error
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-0035 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-0035HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-0035 is a logic error in MediaProvider.java's createRequest() method, disclosed in the Android Security Bulletin for March 2026. The flaw allows a locally-installed application to acquire read/write access to files that do not yet exist in the MediaStore database, bypassing the access control model that MediaProvider enforces for external storage. The CVSS score of 8.4 (HIGH) reflects the lack of any privilege prerequisite and the absence of required user interaction — a zero-click local escalation.

MediaProvider is the privileged content provider that arbitrates all structured access to external storage on Android. It runs under the u:r:mediaprovider:s0 SELinux domain and holds android.permission.MANAGE_MEDIA and direct Android/data access that normal apps cannot obtain. Gaining write authority through MediaProvider is therefore not merely a file access bug — it is a capability escalation into a trusted, system-privileged process.

Affected Component

  • File: packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
  • Method: createRequest() — the internal handler for MediaStore.createWriteRequest() and MediaStore.createDeleteRequest() user-consent flows introduced in Android 11.
  • Affected versions: See NVD / Android Security Bulletin March 2026 for exact SPL ranges.
  • SELinux context: u:r:mediaprovider:s0
  • Process boundary: Runs inside a separate APK process, not system_server, but shares storage grants with the framework.

Root Cause Analysis

The createRequest() path validates each URI supplied by a calling app against the MediaStore database before issuing a pending user-consent intent. The logic error lies in how the method handles URIs that resolve to no existing database row: instead of rejecting the URI outright, a missing-row condition falls through a conditional guard, and the method constructs a grant nonetheless — operating on a phantom path that has never been inserted, scanned, or validated by MediaProvider's security model.

Root cause: createRequest() skips ownership and existence validation when a supplied MediaStore URI resolves to zero database rows, constructing a read/write grant for an arbitrary, non-existent file path that the caller fully controls.

The following pseudocode is reconstructed from AOSP source and decompilation of the affected MediaProvider APK. Comments mark the exact fault:

/*
 * MediaProvider.java — createRequest() — simplified pseudocode
 * Handles MediaStore.createWriteRequest / createDeleteRequest
 */
PendingIntent createRequest(ContentResolver resolver, int type, List uris) {

    ArrayList validatedUris = new ArrayList<>();

    for (Uri uri : uris) {
        // Resolve the URI to a database row
        Cursor c = resolver.query(uri,
                new String[]{ MediaColumns._ID,
                              MediaColumns.OWNER_PACKAGE_NAME,
                              MediaColumns.DATA },
                null, null, null);

        // BUG: no else-branch — if cursor is empty (file does not exist),
        // execution falls through to validatedUris.add(uri) without
        // any ownership check or existence assertion.
        if (c != null && c.moveToFirst()) {
            String owner = c.getString(1 /* OWNER_PACKAGE_NAME */);

            // Ownership check only reached when row EXISTS
            if (!callingPackage.equals(owner)) {
                // Correctly gate third-party URIs behind user consent
                requiresConsent = true;
            }
        }
        // BUG: uri added unconditionally — non-existent file URIs bypass
        //      the ownership block entirely, receiving implicit grant.
        validatedUris.add(uri);   // <-- should be inside the if-block,
                                  //     or preceded by: if (c == null ||
                                  //     !c.moveToFirst()) continue;

        if (c != null) c.close();
    }

    // Grant constructed for ALL entries in validatedUris, including
    // phantom URIs whose paths were never validated.
    return buildGrantIntent(type, validatedUris);
}

The buildGrantIntent() call produces a PendingIntent carrying a Uri permission grant. On Android 11+, this grant is delivered directly to the requesting package without requiring the user-consent dialog when requiresConsent remains false — which it does for non-existent URIs, because the ownership-check block is never entered.

Exploitation Mechanics

EXPLOIT CHAIN — CVE-2026-0035:

1. Attacker app (no special permissions) constructs a synthetic MediaStore
   URI pointing to a non-existent path:
     content://media/external/images/media/99999999
   where row ID 99999999 has never been inserted into the MediaStore DB.

2. App calls MediaStore.createWriteRequest(resolver, List.of(phantomUri)).
   This internally invokes MediaProvider.createRequest() under the
   mediaprovider SELinux domain.

3. createRequest() queries the DB for row 99999999 → cursor returns empty.
   Existence/ownership guard is not entered. requiresConsent stays false.
   phantomUri is appended to validatedUris unconditionally.

4. buildGrantIntent() constructs a PendingIntent granting
   FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION
   for the phantom URI with no user-consent dialog presented.

5. Attacker app receives and fires the PendingIntent, obtaining a live
   Uri grant scoped to the MediaProvider process.

6. App uses ContentResolver.openOutputStream(phantomUri) through the
   granted permission. MediaProvider, operating under its elevated
   domain, creates and writes the file at the path encoded in the URI —
   a path the attacker controls by crafting the URI's DATA column hint
   or by using a pre-seeded but unscanned path under /sdcard/.

7. By targeting a path such as /sdcard/Android/data//,
   the attacker writes arbitrary content into a directory normally
   inaccessible to third-party apps, achieving local privilege
   escalation without any additional permissions or user interaction.

Step 6 is particularly powerful because MediaProvider's openOutputStream() runs as the provider process, which holds MANAGE_EXTERNAL_STORAGE-equivalent filesystem access. Writing into Android/data/ subdirectories of other packages, or into /sdcard/DCIM/ without triggering the scoped-storage model, represents a meaningful escalation beyond the calling app's own sandbox.

Memory Layout

This is a logic-error vulnerability, not a memory corruption issue. The relevant "state" is the permission grant table maintained by the ActivityManagerService UriGrantsManager and the MediaProvider internal DB cursor lifecycle:

GRANT TABLE STATE — BEFORE EXPLOIT:

  UriPermission records for com.attacker.app:
  [ EMPTY — no grants issued ]

  MediaStore DB row lookup for URI /99999999:
  [ SELECT result: 0 rows — file does not exist ]

  requiresConsent flag: false (default)
  validatedUris list:   []

─────────────────────────────────────────────────────────────

GRANT TABLE STATE — AFTER createRequest() LOGIC ERROR:

  validatedUris list (post-loop):
  [ content://media/external/images/media/99999999 ]
         ^--- phantom URI, no DB row, no ownership check

  PendingIntent constructed with:
    flags  = FLAG_GRANT_READ_URI_PERMISSION
            | FLAG_GRANT_WRITE_URI_PERMISSION
    target = com.attacker.app
    uris   = [ content://media/external/images/media/99999999 ]

  UriPermission records for com.attacker.app (after PI fires):
  [ GRANT: content://media/external/images/media/99999999 ]
    mode  = r/w
    owner = android  (MediaProvider, not the calling package)
         ^--- escalated capability, bypasses scoped storage

Patch Analysis

The correct fix enforces that a URI must resolve to an existing, owned database row before being admitted to validatedUris. Non-existent URIs must be rejected with a hard continue or throw rather than silently passing through:

// BEFORE (vulnerable):
for (Uri uri : uris) {
    Cursor c = resolver.query(uri, PROJECTION, null, null, null);

    if (c != null && c.moveToFirst()) {
        String owner = c.getString(COL_OWNER);
        if (!callingPackage.equals(owner)) {
            requiresConsent = true;
        }
    }
    // BUG: unconditional add — phantom URIs bypass ownership check
    validatedUris.add(uri);

    if (c != null) c.close();
}

// AFTER (patched — March 2026 SPL):
for (Uri uri : uris) {
    Cursor c = resolver.query(uri, PROJECTION, null, null, null);

    // FIX: reject URIs that do not correspond to existing DB rows.
    // A non-existent file has no ownership record; granting access
    // to it is meaningless and dangerous.
    if (c == null || !c.moveToFirst()) {
        if (c != null) c.close();
        throw new IllegalArgumentException(
            "Uri does not correspond to an existing media item: " + uri);
    }

    String owner = c.getString(COL_OWNER);
    if (!callingPackage.equals(owner)) {
        requiresConsent = true;
    }

    // Only reached for verified, existing rows
    validatedUris.add(uri);
    c.close();
}

An alternative hardening approach seen in similar AOSP fixes is to explicitly assert c.getCount() == 1 before processing, preventing ambiguous multi-row cursor states from also producing unexpected grants. The patch additionally adds a log entry at Log.w(TAG, "createRequest: rejecting non-existent URI " + uri) which aids in post-incident detection.

Detection and Indicators

Because this is a silent logic error with no crash or exception in the vulnerable path, runtime detection requires active monitoring:

  • Logcat pattern (patched devices): MediaProvider: createRequest: rejecting non-existent URI — presence on unpatched devices is impossible; its absence on patched devices under probe indicates clean state.
  • ADB grant inspection: adb shell dumpsys activity permissions | grep media — look for UriPermission records held by non-system packages targeting content://media/ paths with anomalously high row IDs.
  • Scoped storage anomaly: Files appearing under /sdcard/Android/data/<pkg>/ for a package that has not itself created them, detectable via adb shell ls -la ownership audit.
  • Static analysis: Any APK calling MediaStore.createWriteRequest() or MediaStore.createDeleteRequest() with URIs not previously returned by a MediaStore.Images.Media.query() against the same device should be treated as suspicious.
  • SELinux audit log: Unexpected avc: granted { write } entries from a third-party app UID accessing mediaprovider-owned file descriptors in /sdcard/Android/data/.

Remediation

  • Apply the March 2026 Android Security Patch Level (2026-03-01) or later. This is the only complete fix.
  • OEM/ODM integrators: Verify the patch is cherry-picked into any downstream MediaProvider fork. The file to audit is packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java, method createRequest().
  • Enterprise MDM: Until patching is possible, consider restricting installation of unknown APKs (DISALLOW_INSTALL_UNKNOWN_SOURCES) to reduce the attacker's ability to sideload a malicious app. Note this does not prevent exploitation by a Play Store app that incorporates this technique.
  • App developers: Do not pass self-constructed numeric row ID URIs to MediaStore.createWriteRequest(). Always use URIs returned directly from a ContentResolver.query() against MediaStore.Images.Media.EXTERNAL_CONTENT_URI or equivalent collections.
  • Defenders: Enable adb shell setprop log.tag.MediaProvider VERBOSE on test devices to surface the new rejection log entries and verify patch presence.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →