Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Browser
DescriptionIncorrect authorization in Browser
ComponentBrowser
Sub-componentUI>Browser
Bug ClassAuthorization Error
Tracker498770931
Fix commitb8e07cea42d1 (chromium/src) +64/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

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

Files Changed

  • ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
  • ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
From b8e07cea42d12e323c69817a86e37be5b296370e Mon Sep 17 00:00:00 2001
From: Lukasz Anforowicz <lukasza@chromium.org>
Date: Tue, 01 Sep 2026 10:36:25 -0700
Subject: [PATCH] Normalize URI scheme for SelectFileDialog intent results

Uri.getScheme() preserves case, so URIs returned from an external picker
with a non-lowercase scheme (e.g. "CONTENT://..." or "File://...") were
not recognised by the case-sensitive comparisons against
ContentResolver.SCHEME_FILE and SCHEME_CONTENT in onIntentCompleted()
and GetDisplayNameTask, and were handled inconsistently.

Call Uri.normalizeScheme() on intent-supplied URIs before comparing
their scheme, and use the normalised URI for the single-result
content-scheme check.

TAG=agy
CONV=3b18385a-9216-4e1f-981e-e1ef806c511c

Bug: 498770931
Change-Id: I3366a86eab386023fa78eb41f69ebf3b8792c782
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8330128
Reviewed-by: Finnur Thorarinsson <finnur@chromium.org>
Commit-Queue: Łukasz Anforowicz <lukasza@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689941}
---
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 77e6992..1c3347f 100644
--- a/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
+++ b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
@@ -1015,7 +1015,7 @@
 
             Uri[] filePathArray = new Uri[itemCount];
             for (int i = 0; i < itemCount; ++i) {
-                filePathArray[i] = clipData.getItemAt(i).getUri();
+                filePathArray[i] = clipData.getItemAt(i).getUri().normalizeScheme();
                 // Check if the caller has permission to access the uri if it is a content uri.
                 if (ContentResolver.SCHEME_CONTENT.equals(filePathArray[i].getScheme())
                         && !doesCallerHavePermissionForUri(filePathArray[i])) {
@@ -1030,6 +1030,7 @@
             return;
         }
 
+        uri = uri.normalizeScheme();
         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
             String filePath = uri.getPath();
             if (!TextUtils.isEmpty(filePath)) {
@@ -1041,7 +1042,7 @@
             }
         }
 
-        if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) {
+        if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
             // Check if the caller has permission to access the uri.
             if (!doesCallerHavePermissionForUri(uri)) {
                 onFileNotSelected();
@@ -1280,7 +1281,10 @@
         public GetDisplayNameTask(Context context, boolean isMultiple, Uri[] uris) {
             mContext = context;
             mIsMultiple = isMultiple;
-            mUris = uris;
+            mUris = new Uri[uris.length];
+            for (int i = 0; i < uris.length; i++) {
+                mUris[i] = uris[i].normalizeScheme();
+            }
             mFilePaths = new String[mUris.length];
         }
 
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 f826def..b3590e5 100644
--- a/ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
+++ b/ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.app.Activity;
+import android.content.ClipData;
 import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -956,6 +957,62 @@
     }
 
     @Test
+    public void testMultipleFileSelectorWithCaseVariantFileUris() {
+        SelectFileDialog selectFileDialog = new SelectFileDialog(0);
+        Uri[] filePathArray =
+                new Uri[] {
+                    Uri.parse("FILE:///storage/emulated/0/DCIM/Camera/IMG_0.jpg"),
+                    Uri.parse("File:///storage/emulated/0/DCIM/Camera/IMG_1.jpg")
+                };
+        SelectFileDialog.GetDisplayNameTask task =
+                selectFileDialog
+                .new GetDisplayNameTask(ContextUtils.getApplicationContext(), true, filePathArray);
+        task.doInBackground();
+        assertEquals("///storage/emulated/0/DCIM/Camera/IMG_0.jpg", task.mFilePaths[0].toString());
+        assertEquals("///storage/emulated/0/DCIM/Camera/IMG_1.jpg", task.mFilePaths[1].toString());
+    }
+
+    @Test
+    public void testMultipleFileSelectorWithCaseVariantAppDirUris() {
+        SelectFileDialog selectFileDialog = new SelectFileDialog(0);
+        File dataDir = ContextCompat.getDataDir(ContextUtils.getApplicationContext());
+        File appFile = new File(dataDir, "app_data.txt");
+        Uri[] filePathArray = new Uri[] {Uri.parse("FILE://" + appFile.getAbsolutePath())};
+        SelectFileDialog.GetDisplayNameTask task =
+                selectFileDialog
+                .new GetDisplayNameTask(ContextUtils.getApplicationContext(), true, filePathArray);
+        assertEquals(null, task.doInBackground());
+    }
+
+    @Test
+    @DisableFeatures({UiAndroidFeatures.CHECK_INTENT_CALLER_PERMISSION})
+    public void testIntentCompletedWithCaseVariantClipDataUri() {
+        TestSelectFileDialog selectFileDialog = new TestSelectFileDialog(0);
+        ClipData clipData =
+                ClipData.newUri(
+                        ContextUtils.getApplicationContext().getContentResolver(),
+                        "label",
+                        Uri.parse("FILE:///storage/emulated/0/DCIM/Camera/IMG_0.jpg"));
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setClipData(clipData);
+        selectFileDialog.onIntentCompleted(Activity.RESULT_OK, intent);
+        runAllAsyncTasks();
+        assertEquals(1, selectFileDialog.mFileSelectionSuccess);
+        assertEquals(0, selectFileDialog.mFileSelectionAborted);
+    }
+
+    @Test
+    @DisableFeatures({UiAndroidFeatures.CHECK_INTENT_CALLER_PERMISSION})
+    public void testIntentCompletedWithCaseVariantDataUri() {
+        TestSelectFileDialog selectFileDialog = new TestSelectFileDialog(0);
+        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("CONTENT://com.example.test/xyz"));
+        selectFileDialog.onIntentCompleted(Activity.RESULT_OK, intent);
+        runAllAsyncTasks();
+        assertEquals(1, selectFileDialog.mFileSelectionSuccess);
+        assertEquals(0, selectFileDialog.mFileSelectionAborted);
+    }
+
+    @Test
     public void testMultipleFileSelectorWithSchemelessUris() {
         SelectFileDialog selectFileDialog = new SelectFileDialog(0);
         Uri[] filePathArray =
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential bypass of data directory access protection in SelectFileDialog via schemeless URIs

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.

Overview: SelectFileDialog.java uses scheme comparisons to identify content:// and file:// URIs for security validation, allowing a malicious app acting as a file picker to bypass these checks by providing a schemeless URI in ClipData. This results in the unvalidated, sensitive path being sent to the browser process, which grants the renderer read access. A malicious website can then use the JavaScript File API to read and exfiltrate Chrome’s private data.

Affected files:

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

Estimated timestamp from git blame: 2022-06-17

Overview

There is a potential vulnerability in how Chrome on Android handles file selection results from third-party applications. SelectFileDialog.java implements security checks to prevent malicious file picker apps from tricking Chrome into reading its own sensitive data (like /data/data/...). However, these checks rely on examining the URI scheme. By returning a ClipData item containing a schemeless URI, a malicious app can completely bypass these security validations, leading to arbitrary local file read access for the renderer process.

Vulnerability Details

When a user interacts with an <input type="file" multiple> element on a web page, Chrome dispatches an Intent.ACTION_GET_CONTENT. If the user selects a malicious file picker app, that app can return a crafted Intent containing ClipData with a schemeless URI pointing to a sensitive Chrome file, e.g., Uri.parse("/data/data/com.android.chrome/shared_prefs/com.android.chrome_preferences.xml").

In ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java, the method onIntentCompleted processes the ClipData. It iterates through the items and checks for permissions, but this check is guarded by a scheme verification:

if (ContentResolver.SCHEME_CONTENT.equals(filePathArray[i].getScheme()))

Because the malicious URI has no scheme, getScheme() returns null, and the permission check is bypassed.

The URIs are then passed to GetDisplayNameTask.doInBackground(). Here, the code decides which directory safety check to apply (isPathUnderAppDir or isContentUriUnderAppDir) based on the scheme:

if (ContentResolver.SCHEME_FILE.equals(mUris[i].getScheme())) {
    if (isPathUnderAppDir(mUris[i].getSchemeSpecificPart(), mContext)) return null;
    mFilePaths[i] = mUris[i].getSchemeSpecificPart();
} else {
    if (ContentResolver.SCHEME_CONTENT.equals(mUris[i].getScheme())
            && isContentUriUnderAppDir(mUris[i], mContext)) return null;
    mFilePaths[i] = mUris[i].toString();
}

For a schemeless URI, both SCHEME_FILE and SCHEME_CONTENT equality checks fail. Execution falls through to the else block, bypasses the isContentUriUnderAppDir check, and directly assigns the raw, unvalidated path (mUris[i].toString()) to mFilePaths[i].

This unvalidated absolute path is passed via JNI to the browser process (SelectFileDialogImpl::OnMultipleFilesSelected). The browser process then calls ChildProcessSecurityPolicyImpl::GrantReadFile, explicitly granting the sandboxed renderer process permission to read the file. Finally, the JavaScript on the malicious website receives a File object and can use FileReader.readAsText() to read and exfiltrate the sensitive data.

Potential Steps to Reproduce

Note: These are suggested steps to trigger the vulnerability. Our tooling agent cannot run code to produce a working PoC.

  1. An attacker publishes a malicious Android app that registers to handle ACTION_GET_CONTENT intents.
  2. The user visits an attacker-controlled webpage in Chrome for Android.
  3. The webpage prompts the user to select files using an <input type="file" multiple> element.
  4. The user selects the attacker’s malicious app from the Android intent chooser.
  5. The malicious app returns an Activity.RESULT_OK intent. The intent has a null Data field but includes a ClipData object. One of the ClipData items contains the schemeless URI: Uri.parse("/data/data/com.android.chrome/shared_prefs/com.android.chrome_preferences.xml").
  6. Chrome processes the intent, bypasses the directory safety checks due to the missing scheme, and passes the path to the native browser process.
  7. The browser process grants the renderer read access to the file.
  8. The attacker’s webpage uses FileReader or fetch() on the resulting File object to read the contents of com.android.chrome_preferences.xml and sends it to their server.

Suggested Fix

In GetDisplayNameTask.doInBackground(), all URIs should be subjected to the isPathUnderAppDir check, regardless of their scheme. If a URI’s string representation or resolved path falls under the application’s data directory, it should be rejected. Alternatively, the code should explicitly reject any URI that does not possess an expected scheme (file or content).

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker