CVE-2026-106313
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forui/android/java/src/org/chromium/ui/base/SelectFileDialog.java |
modified |
Files Changed
ui/android/junit/src/org/chromium/ui/base/SelectFileDialogTest.javaui/android/java/src/org/chromium/ui/base/SelectFileDialog.java
Patch
From b8e07cea42d12e323c69817a86e37be5b296370e Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz <[email protected]> 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 <[email protected]> Commit-Queue: Łukasz Anforowicz <[email protected]> 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 =
Original Bug Report
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.
- An attacker publishes a malicious Android app that registers to handle
ACTION_GET_CONTENTintents. - The user visits an attacker-controlled webpage in Chrome for Android.
- The webpage prompts the user to select files using an
<input type="file" multiple>element. - The user selects the attacker’s malicious app from the Android intent chooser.
- The malicious app returns an
Activity.RESULT_OKintent. The intent has anullData field but includes aClipDataobject. One of theClipDataitems contains the schemeless URI:Uri.parse("/data/data/com.android.chrome/shared_prefs/com.android.chrome_preferences.xml"). - Chrome processes the intent, bypasses the directory safety checks due to the missing scheme, and passes the path to the native browser process.
- The browser process grants the renderer read access to the file.
- The attacker’s webpage uses
FileReaderorfetch()on the resultingFileobject to read the contents ofcom.android.chrome_preferences.xmland 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.