CVE-2025-32327 AI-generated analysis
platform/packages/providers/MediaProvider, Photo Picker database facade (PickerDbFacade)
PickerDbFacade no longer embeds unrecognized, caller-supplied projection column names into the SQL projection of queryMediaIdForApps; unknown columns are dropped from the SQL query and re-added as null-valued columns in a MatrixCursor built after the query.
Root cause
In PickerDbFacade.getMediaStoreProjectionLocked, the default branch of the column switch handled any unsupported column name by emitting projection[i] = getProjectionSimple("NULL", columns[i]), i.e. a projection entry built from NULL and the raw column string supplied by the caller. The column string was not validated before being placed in the projection array passed to SQLiteQueryBuilder.query in queryMediaIdForAppsInternal, so a caller-chosen string could become part of the SQL projection. The test change from "testInvalidColumn" to "test invalid column" (not a bare SQL identifier) suggests that column names previously reached the SQL layer.
How the fix works
- PickerDbFacade.java:getMediaStoreProjectionLocked: the fixed-size String[] projection is replaced by an ArrayList<String>; the default branch no longer adds any projection entry for an unrecognized column (only the Log.w remains, with the comment updated to mention ACTION_GET_CONTENT takeover). Unknown column names therefore no longer reach the SQL statement.
- PickerDbFacade.java:queryMediaIdForAppsInternal (parameter renamed projection -> columns): after running qb.query with the filtered projection, it compares cursor.getColumnCount() to columns.length. If equal (or columns is null/empty) the raw cursor is returned unchanged. Otherwise it builds a MatrixCursor over the originally requested columns, iterates the result cursor, copies each row into a ContentValues via DatabaseUtils.cursorRowToContentValues, and adds a row with contentValues.get(column) for every requested column (null for the unknown ones), then closes the original cursor and returns the MatrixCursor. This keeps unknown columns present with null values, without involving SQL.
- PickerDbFacadeTest.java: the invalid column test now uses a name with spaces, asserts the column count is 2, that getLong on the unknown column is 0 and getString is null, and that DATE_TAKEN is still returned correctly.
Completeness
- Covers the only place in the diff where caller-provided column names were turned into SQL text: the default branch of getMediaStoreProjectionLocked, which feeds queryMediaIdForAppsInternal. All recognized PickerMediaColumns continue to map to fixed, internally defined projection strings.
- The MatrixCursor rebuild is driven purely by a count comparison (cursor.getColumnCount() == columns.length); since unknown columns are now always dropped, any mismatch means at least one unknown column was requested.
- The null check on columns inside queryMediaIdForAppsInternal comes after getMediaStoreProjectionLocked has already dereferenced columns.length, so it is effectively dead; callers passing null would still NPE as before (parameter is @NonNull).
- The material does not show other query paths in PickerDbFacade or their projection handling, so whether a similar pattern exists elsewhere cannot be judged from the diff.
Behavior changes
- Unrecognized projection columns are no longer part of the executed SQL; the warning log 'Unexpected Picker column' is still emitted.
- When at least one unknown column is requested, the returned Cursor is a MatrixCursor rather than the SQLite cursor, with values copied through ContentValues; the test shows getLong returns 0 and getString returns null for the unknown column.
- When all requested columns are recognized, behavior is unchanged (the original cursor is returned directly).
Per major
- Android 14: same fix: commit 186cff75881c in android-security-14.0.0_r22 (RESTRICT AUTOMERGE cherry-pick chain), modifying queryMediaIdForAppsInternal and getMediaStoreProjectionLocked; at android-security-14.0.0_r29 7/8 lines of the patched region are present.
- Android 15: adapted fix: commit 3f48b77e7cb9 in android-security-15.0.0_r10 applies the same logic to queryMediaIdForAppsLocked and getMediaStoreProjectionLocked(columns, pickerSegmentType), which carry an extra pickerSegmentType parameter on 15; at android-security-15.0.0_r17 7/8 lines present.
- Android 16: not affected (not listed by the bulletin); at android-security-16.0.0_r8 6/8 lines of the patched region are present; no commit shown in the material.
- Android 17: not affected (not listed by the bulletin); at android-security-17.0.0_r1 6/8 lines of the patched region are present; no commit shown in the material.
Unknowns
- The body of getProjectionSimple is not in the material; that it produced a projection containing the raw column string unquoted, and thus that the flaw is SQL injection, is inferred from its usage and the test change.
- The external caller(s) of queryMediaIdForApps and how an app's projection reaches it (and what permission or URI checks sit in front of it) are not shown.
- Whether type reporting (getType) of the MatrixCursor differs from the original SQLite cursor is not shown.
- Whether other PickerDbFacade query methods have a comparable default-column branch is not visible in the material.
- The Android 16/17 code state is only shown as line-presence counts.