CVE-2026-28638 AI-generated analysis
platform/packages/providers/MediaProvider, media metadata redaction (util/IsoInterface, util/XmpInterface, util/XmpDataParser)
MediaProvider's XMP redaction for ISO (MP4-style) media is extended to redact EXIF GPS fields stored as XML attributes (not only as element names) and, on Android 14, to process every XMP box in the container rather than only the first one.
Root cause
XmpInterface (Android 14) and XmpDataParser (15+) decided whether to redact an XMP element only by testing the element's own namespace and name against NS_EXIF and the redacted EXIF tag set. XMP allows the same properties (e.g. GPSLatitude) to be expressed as attributes on an element; those were never matched, so they were not redacted for callers without ACCESS_MEDIA_LOCATION (per commit message). On Android 14, XmpInterface.fromContainer also extracted only the first XMP UUID box (be7acfcb-97a9-42e8-9c71-999491e3afac) or, if none, the first BOX_XMP box via IsoInterface.getBoxBytes, so further XMP boxes were never scanned. On Android 16/17, IsoInterface's filtered flatten switch did not keep BOX_XMP boxes, so plain XMP boxes were not found by the lookup.
How the fix works
- IsoInterface.java (14): adds getBoxRangesList(int)/getBoxRangesList(UUID) and getBoxBytesList(int)/getBoxBytesList(UUID). Each iterates mFlattened and returns one entry per matching box ([range[0]+headerSize, range[0]+range[1]] and box.data respectively), unlike the first-match-only getBoxBytes variants.
- XmpInterface.java (14): the constructor body moves into a new process(rawXmp, redactedExifTags, xmpOffsets) that can be called repeatedly on one instance; mRedactedXmp is assigned only on the first call. In the START_TAG loop, before the rdf:Description/EXIF name checks, it walks parser.getAttributeCount() and sets hasSensitiveAttribute when an attribute has namespace NS_EXIF and a name in redactedExifTags. If set, it reads tokens until the matching END_TAG (depth counting), records [xmpOffset+start, xmpOffset+offset] in mRedactedRanges, overwrites that span in rawXmp with spaces, and continues. The existing element-name redaction path now also writes into rawXmp (the current box) instead of mRedactedXmp.
- XmpInterface.fromContainer (14): collects all UUID XMP boxes, then all BOX_XMP boxes, into bufs/xmpOffsetsList. It builds XmpInterface from the first one (or empty arrays if there are none) and calls process() for each remaining box, so every XMP box adds redaction ranges.
- XmpDataParser.java (15/16/17): adds isSensitiveTagOrAttribute(), which returns true if the element itself is NS_EXIF plus a redacted tag, or if any attribute is NS_EXIF with a redacted tag name. The metadata-parsing loop and the redacted-ranges loop both call it instead of the inline element-only check. In the parsing loop the check is no longer an else-if, so it runs after format/DocumentID handling for the same element.
- IsoInterface.java (15): restructured so parseNextBox adds only selected box types (BOX_UUID with XMP_UUID, BOX_META, BOX_HDLR, BOX_XYZ, BOX_XMP, BOX_LOCI, BOX_GPS, BOX_GPS0) to mFlattened. It introduces MAX_XMP_SIZE_BYTES (1 MiB) through allocateBuffer for XMP data, and replaces getBoxBytes(UUID)/getBoxRanges(UUID) with getBoxBytesForXmpUuid()/getBoxRangesForXmpUuid(), which use a fixed XMP_UUID constant. XmpData.extractXmpData is updated to the new names.
- IsoInterface.java (16/17): adds BOX_XMP to the switch that keeps boxes in the flattened list, so getBoxBytes(BOX_XMP) finds plain XMP boxes.
- Tests: a new tests/res/raw/gps_test_xmp.mp4 and testRedactXmp_RemovesExifGpsAttributes, which asserts that the redacted XMP no longer contains exif:GPSLatitude or exif:GPSLongitude.
Completeness
- Attribute matching covers only attributes in namespace NS_EXIF whose local name is in the redacted EXIF tag set. Sensitive values under other namespaces are not checked (same scope as the existing element-name check).
- Android 14: mRedactedRanges covers all XMP boxes, but mRedactedXmp (returned by getRedactedXmp) is only the first box's buffer. Later boxes are redacted in place in the IsoInterface box.data arrays but are not part of the returned XMP copy.
- Android 14: getBoxRangesList uses only range[0]/range[1] of each box (one range per box), unlike the older getBoxRanges, which iterated all range pairs.
- Android 15/16/17: XmpData.extractXmpData still takes only the first XMP UUID box and falls back to the first BOX_XMP box. The 15 diff shows this, and 16/17 end with the same XmpDataParser blob (3e48d1d91). The multi-box handling added on Android 14 is not in these diffs.
- Android 14: when a sensitive attribute is found, the whole enclosing element (children included) is redacted and the loop continues before the rdf:Description parsing, so non-sensitive metadata in that element is also dropped. This is over-redaction, not a gap.
Behavior changes
- Android 14: an element carrying an NS_EXIF attribute in the redacted set is now blanked in full (span up to its matching end tag) in the current XMP buffer and in the file-level redaction ranges. On 15/16/17 such elements now enter the existing XmpDataParser redaction path.
- Android 14: on an element with a sensitive attribute, dc:format, DocumentID, OriginalDocumentID and InstanceID in that same element are no longer extracted (the loop skips the element).
- Android 14: redaction ranges may now span several XMP boxes within one file.
- Android 15: IsoInterface keeps only a fixed set of box types in mFlattened, so getBoxBytes/getBoxRanges return nothing for other types. XMP boxes larger than 1 MiB make allocateBuffer return null and parseNextBox return false, which stops parsing of the remaining sibling boxes at that level.
- Android 16/17: BOX_XMP boxes are now kept and parsed, so their content goes through redaction.
Per major
- Android 14: Fixed in android-security-14.0.0_r29 by 15168c2e6f84 (the commit message lists two Cherrypick-From sources). It adds list-returning box accessors to IsoInterface, plus attribute scanning and multi-box process() in XmpInterface. The patched region is fully present (8/8 lines).
- Android 15: Adapted fix cb04f3531ff2 in android-security-15.0.0_r17. The attribute check is XmpDataParser.isSensitiveTagOrAttribute(), and IsoInterface is restructured (type-filtered flatten list, 1 MiB XMP cap, XMP_UUID helpers). No multi-box handling in this diff.
- Android 16: Adapted fix 4bdbc2fa9f53 in android-security-16.0.0_r8: XmpDataParser.isSensitiveTagOrAttribute(), plus BOX_XMP added to IsoInterface's retained-box switch. No multi-box handling in this diff.
- Android 17: Same adapted fix as 16 (11009f3800f9 in android-security-17.0.0_r1); the diff is identical, with the same blob hashes.
Unknowns
- The material does not show which MediaProvider entry point (redaction info computation) calls XmpInterface.fromContainer / XmpDataParser.createXmpInterface.
- Whether Android 15/16/17 handle multiple XMP boxes elsewhere, or whether the single-box behaviour there is intentional.
- The full span logic of the existing XmpDataParser redaction block that attribute matches now enter on 15/16/17.
- Contents of gps_test_xmp.mp4 beyond what the test asserts.