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.
A newly discovered security flaw affects how Android devices handle file access requests. Think of it like a lock on a filing cabinet that doesn't properly check who's trying to open it — a misbehaving app could trick the system into letting it read or modify files it shouldn't be able to touch.
Here's what's happening technically, but in plain terms: Android has a system called MediaProvider that manages access to photos, videos, and music files. One part of this system, called createRequest, is supposed to verify that apps only access files they're allowed to see. But researchers found a gap in that verification — apps can use clever path tricks to access files that don't even exist yet, or worse, create new access to files they have no business touching.
This matters because your phone stores genuinely sensitive stuff: banking apps, encrypted messages, location history, health data. A bad actor could create an app that exploits this flaw to peek at your files or modify them without you ever knowing or giving permission. You wouldn't see any obvious warning — it just happens in the background.
Who's most at risk? Anyone using affected Android devices, particularly if they sideload apps from outside Google Play or install apps from less-trusted sources. The good news: there's no evidence this is being actively exploited in the wild yet, giving phone makers time to patch it.
What you can do right now: First, keep your Android device updated — manufacturers will release fixes soon. Second, only install apps from Google Play, which has more security screening than random websites. Third, periodically review which apps have file access permissions by going to Settings, Apps, and checking what each one can access. You don't need every app reading your photo library.
Want the full technical analysis? Click "Technical" above.
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.
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.