High chrome Input Validation 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in UI
DescriptionImproper input validation in UI
ComponentUI
Sub-componentAndroid
Bug ClassInput Validation
Tracker574803484
Fix commit9dffe99d209a (chromium/src) +80/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-27

Background

`SelectFileDialog`
Android file-picker glue (ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java) that converts the URIs returned by the Android picker into file paths handed to the browser for opening.
`file:` URI forms
a hierarchical file:///path URI has a real path component; an opaque (non-hierarchical) file: URI such as file:content://authority/path has no path and only a scheme-specific part. Relative file: URIs (file:valid.jpg) likewise lack an absolute path.
`Uri.getPath()` vs `Uri.getSchemeSpecificPart()`
getPath() returns the hierarchical path (or null for opaque URIs); getSchemeSpecificPart() returns everything after the scheme, so for file:content://... it returns the bare content://... string.
`resolveRegularFilePath()`
canonicalizes a caller-supplied path with realpath() and returns it only if it names an existing regular file outside procfs.
`GetDisplayNameTask` / `onIntentCompleted()`
two code paths that process picker results; the former extracted the path via getSchemeSpecificPart(), the latter already used getPath(), so they disagreed.

Root Cause Analysis

The two paths that turn a file: URI into a path were inconsistent, and the inconsistency was security-relevant. In GetDisplayNameTask, a file: URI was reduced to a path via mUris[i].getSchemeSpecificPart(). For an opaque file: URI like file:content://com.android.chrome.FileProvider/images/pic.jpg, that call returns the string content://com.android.chrome.FileProvider/images/pic.jpg — a bare content URI, not a filesystem path. As the added comment explains, native would then open that string through ContentResolver, bypassing the content-URI checks that the code performs for legitimately content-scheme URIs. In other words, an attacker-influenced picker result could wrap a content URI inside an opaque file: URI to slip past the content-URI access controls, and relative file: URIs (file:valid.jpg) could likewise reach realpath() and resolve to unintended files. resolveRegularFilePath() compounded this by only checking TextUtils.isEmpty(path) before canonicalizing, so any non-absolute string was accepted into FileUtils.getAbsoluteFilePath().

The patch enforces that a file: URI must be hierarchical and absolute. GetDisplayNameTask now calls mUris[i].getPath() (returning null for opaque URIs) to match onIntentCompleted(), and resolveRegularFilePath() rejects any path that is empty or does not start with / (!path.startsWith("/")), its parameter made @Nullable. Together these reject opaque file:content://… URIs and relative paths before path canonicalization, so a content URI can no longer be laundered into the file path and must go through the proper content-URI checks. The advisory’s label Improper input validation in UI matches: this is missing validation of the URI shape that enabled a check bypass. The new testFileSchemeRejectsOpaqueAndRelativeUris test asserts opaque/relative URIs are rejected end-to-end and never reach getAbsoluteFilePath(), while a valid hierarchical file:/// URI still succeeds.

Key insight
GetDisplayNameTask extracted a file: URI’s path with getSchemeSpecificPart(), so an opaque file:content://… URI yielded a bare content URI that native opened via ContentResolver, bypassing the content-URI access checks — a validation gap fixed by requiring a hierarchical, absolute path.

Attack Path

  1. Craft a URI Attacker (a malicious/colluding app returning an intent result, or any flow that can influence the picker’s returned URIs) supplies an opaque file:content://com.android.chrome.FileProvider/images/pic.jpg. (reachability via intent result inferred from the component)
  2. Path extraction GetDisplayNameTask calls getSchemeSpecificPart(), extracting content://com.android.chrome.FileProvider/images/pic.jpg.
  3. Check bypass Because the string arrived as a file: URI, it skips the content-URI validation and is forwarded as a path; native opens it through ContentResolver.
  4. Unintended access The resolved content URI points at a file/provider the content-URI checks would have blocked (or a relative path resolves to an unintended regular file).
  5. Primitive Access to a file the user did not select / that the access checks would deny, i.e., a content-URI access-control bypass. (strength beyond unintended read inferred)

Impact Assessment

This is Android browser-process UI glue; the attacker surface is the set of URIs that reach SelectFileDialog from the picker/intent result. No memory-safety sink is involved — the defect is a logic/validation gap that lets an opaque file: URI smuggle a content URI past the content-URI checks, yielding unintended file/provider access rather than memory corruption. Privilege required is the ability to influence the returned intent URIs (e.g., a malicious app participating in the picker flow). The concrete gain is reading/forwarding a file or content URI the user did not choose and that the checks would otherwise reject; broader escalation is (inferred). The advisory’s “improper input validation” is accurate for this diff.

Changed Functions

FunctionChangeNotes
for
ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
modified

Files Changed

  • ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
  • ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java

Audit Directions

  • `getSchemeSpecificPart()` on `file:`/`content:` URIs
    Grep Android glue for any URI-to-path conversion using getSchemeSpecificPart(); it returns laundered content for opaque URIs and should generally be getPath().
  • Divergent URI handling between code paths
    Compare every picker/intent-result path (single-file vs. multi-file vs. display-name) for consistent scheme/absolute-path validation; the bug was two paths disagreeing.
  • Missing absolute-path checks
    Review callers of realpath()/getAbsoluteFilePath() for inputs that are not validated as absolute before canonicalization.
  • Opaque-URI smuggling
    Look for other places where a content:/file: URI could be nested to bypass scheme-specific access checks.
From 9dffe99d209ab61d768bfd3f46f55f43cea466b5 Mon Sep 17 00:00:00 2001
From: Calder Kitagawa <ckitagawa@chromium.org>
Date: Mon, 19 Oct 2026 09:04:13 -0700
Subject: [PATCH] [Android] Consistently extract absolute file paths in SelectFileDialog

GetDisplayNameTask previously extracted file: URI paths via
Uri.getSchemeSpecificPart(), whereas single-file selection in
onIntentCompleted() uses Uri.getPath(). Aligning GetDisplayNameTask on
Uri.getPath() and requiring an absolute path in resolveRegularFilePath()
ensures non-hierarchical and relative file: URIs are rejected
consistently before path canonicalization.

Fixed: 574803484
Change-Id: I97cb96f43dcda41065c71fef8a78a4c86a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8611108
Auto-Submit: Calder Kitagawa <ckitagawa@chromium.org>
Reviewed-by: Sky Malice <skym@chromium.org>
Commit-Queue: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1722015}
---
diff --git a/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
index 56e80f4..fa1e565 100644
--- a/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
+++ b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
@@ -1028,11 +1028,12 @@
     }
 
     /**
-     * Canonicalises a caller-supplied file path with realpath() and returns it only if it names an
-     * existing regular file that is not in procfs. Returns null otherwise.
+     * Canonicalises a caller-supplied file path with realpath() and returns it only if it is an
+     * absolute path that names an existing regular file that is not in procfs. Returns null
+     * otherwise.
      */
-    private static @Nullable String resolveRegularFilePath(String path) {
-        if (TextUtils.isEmpty(path)) return null;
+    private static @Nullable String resolveRegularFilePath(@Nullable String path) {
+        if (TextUtils.isEmpty(path) || !path.startsWith("/")) return null;
         String resolved = FileUtils.getAbsoluteFilePath(path);
         // Reject paths such as /proc/self/maps that resolve into procfs.
         if (resolved.isEmpty() || resolved.startsWith("/proc/") || !new File(resolved).isFile()) {
@@ -1401,12 +1402,17 @@
                     // device was observed to return a file:// URI instead, so convert if necessary.
                     // See https://crbug.com/752834 for context.
                     if (ContentResolver.SCHEME_FILE.equals(mUris[i].getScheme())) {
+                        // Only hierarchical file:///path URIs are acceptable here. An opaque
+                        // file: URI such as "file:content://authority/path" has no path, and its
+                        // scheme-specific part is a bare content URI that native would open
+                        // through ContentResolver, bypassing the content URI checks below.
+                        //
                         // Resolve the path *once* (realpath, follows /proc/self/fd/N magic
                         // links and symlinks) and forward the resolved path instead of the
                         // caller-supplied alias, so that later opens by the browser cannot be
                         // redirected to a different file. Reject anything that does not
                         // resolve to an existing regular file outside the app data dir.
-                        String resolved = resolveRegularFilePath(mUris[i].getSchemeSpecificPart());
+                        String resolved = resolveRegularFilePath(mUris[i].getPath());
                         if (resolved == null || isPathUnderAppDir(resolved, mContext)) {
                             return null;
                         }
diff --git a/ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java b/ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
index 81999f05..2f4adf3 100644
--- a/ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
+++ b/ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
@@ -1219,6 +1219,75 @@
     }
 
     @Test
+    @DisableFeatures({UiAndroidFeatures.CHECK_INTENT_CALLER_PERMISSION})
+    public void testFileSchemeRejectsOpaqueAndRelativeUris() throws IOException {
+        File validFile = mTempFolder.newFile("valid.jpg");
+
+        // Even if realpath() were to resolve a non-hierarchical or relative path to an existing
+        // regular file, opaque file: URIs (such as "file:content://...") and relative paths must
+        // be rejected before reaching FileUtils.getAbsoluteFilePath().
+        Mockito.lenient()
+                .doReturn(validFile.getCanonicalPath())
+                .when(mFileUtilsMocks)
+                .getAbsoluteFilePath(any());
+
+        SelectFileDialog selectFileDialog = new SelectFileDialog(0);
+        Uri[] opaqueAndRelativeUris =
+                new Uri[] {
+                    Uri.parse("file:content://com.android.chrome.FileProvider/images/pic.jpg"),
+                    Uri.parse("FILE:content://com.android.chrome.FileProvider/images/pic.jpg"),
+                    Uri.parse("file:valid.jpg"),
+                    Uri.fromParts("file", "valid.jpg", null),
+                };
+        for (Uri uri : opaqueAndRelativeUris) {
+            SelectFileDialog.GetDisplayNameTask task =
+                    selectFileDialog
+                    .new GetDisplayNameTask(
+                            ContextUtils.getApplicationContext(),
+                            /* isMultiple= */ false,
+                            new Uri[] {uri});
+            assertEquals("Expected null for " + uri, null, task.doInBackground());
+        }
+
+        SelectFileDialog.FilePathSelectedTask relativePathTask =
+                selectFileDialog
+                .new FilePathSelectedTask(ContextUtils.getApplicationContext(), "valid.jpg", null);
+        assertFalse(relativePathTask.doInBackground());
+
+        // End-to-end onIntentCompleted checks with opaque file:content:// URI in ClipData and data.
+        TestSelectFileDialog testDialog = new TestSelectFileDialog(0);
+        Uri opaqueContentUri =
+                Uri.parse("file:content://com.android.chrome.FileProvider/images/pic.jpg");
+        Intent clipIntent = new Intent();
+        clipIntent.setClipData(
+                new ClipData("label", new String[0], new ClipData.Item(opaqueContentUri)));
+        testDialog.onIntentCompleted(Activity.RESULT_OK, clipIntent);
+        runAllAsyncTasks();
+        assertEquals(0, testDialog.mFileSelectionSuccess);
+        assertEquals(1, testDialog.mFileSelectionAborted);
+        testDialog.resetFileSelectionAttempts();
+
+        Intent dataIntent = new Intent(Intent.ACTION_VIEW, opaqueContentUri);
+        testDialog.onIntentCompleted(Activity.RESULT_OK, dataIntent);
+        runAllAsyncTasks();
+        assertEquals(0, testDialog.mFileSelectionSuccess);
+        assertEquals(1, testDialog.mFileSelectionAborted);
+        testDialog.resetFileSelectionAttempts();
+        Mockito.verify(mFileUtilsMocks, Mockito.never()).getAbsoluteFilePath(any());
+
+        // Valid hierarchical file:/// URI still succeeds.
+        SelectFileDialog.GetDisplayNameTask validTask =
+                selectFileDialog
+                .new GetDisplayNameTask(
+                        ContextUtils.getApplicationContext(),
+                        /* isMultiple= */ false,
+                        new Uri[] {Uri.fromFile(validFile)});
+        assertTrue(validTask.doInBackground() != null);
+        Mockito.verify(mFileUtilsMocks).getAbsoluteFilePath(validFile.getPath());
+        assertEquals(validFile.getCanonicalPath(), validTask.mFilePaths[0]);
+    }
+
+    @Test
     public void testShowTypes() {
         ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
         shadowMimeTypeMap.addExtensionMimeTypeMapping("jpg", "image/jpeg");
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.