CVE-2026-113099
Overview
Files Changed
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.javachrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandlerUnitTest.javachrome/browser/ui/android/omnibox/BUILD.gn
Patch
From fa14b0941353a1aed78e72c46e810b503fbdaf16 Mon Sep 17 00:00:00 2001 From: Yoshisato Yanagisawa <[email protected]> Date: Fri, 18 Sep 2026 13:21:50 -0700 Subject: [PATCH] Skip own-app content and file URIs in location bar drops Update LocationBarDragDropHandler to filter out file scheme URIs and content scheme URIs originating from the local application when resolving loadable items from drag-and-drop clip data. This aligns location bar drop handling with existing web content drop behavior. This validation is gated behind ClipboardConfusedDeputyDefenseFiles. Standard web navigation URIs, browsable intents, and content URIs from other applications remain unaffected. Fixed: 556227230 Change-Id: I297c1072d4e4a393d94aac4b7c99a2650f27f2ee Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8429627 Auto-Submit: Tomasz Wiszkowski <[email protected]> Commit-Queue: Tomasz Wiszkowski <[email protected]> Reviewed-by: Sky Malice <[email protected]> Cr-Commit-Position: refs/heads/main@{#1701052} --- diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java index 96fd4f0..9c9bff3 100644 --- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java +++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java @@ -17,16 +17,19 @@ import android.view.View.OnDragListener; import android.webkit.MimeTypeMap; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import org.chromium.base.ContentUriUtils; import org.chromium.build.annotations.NullMarked; +import org.chromium.build.annotations.Nullable; import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.TabObserver; import org.chromium.components.embedder_support.util.UrlUtilities; import org.chromium.ui.base.MimeTypeUtils; import org.chromium.ui.base.PageTransition; +import org.chromium.ui.base.UiAndroidFeatureMap; +import org.chromium.ui.base.UiAndroidFeatures; import org.chromium.ui.base.WindowAndroid; import org.chromium.url.GURL; @@ -157,8 +160,7 @@ * @return The mime type, or null if it cannot be determined. */ @VisibleForTesting - @Nullable - String getMimeType(Context context, Uri uri) { + @Nullable String getMimeType(Context context, Uri uri) { if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) { try { return context.getContentResolver().getType(uri); @@ -240,8 +242,8 @@ * @return The first acceptable URI, or null if none is found. */ @VisibleForTesting - @Nullable - Uri findUriToLoad(Context context, ClipData clipData, @Nullable ClipDescription desc) { + @Nullable Uri findUriToLoad( + Context context, ClipData clipData, @Nullable ClipDescription desc) { for (int i = 0; i < clipData.getItemCount(); i++) { ClipData.Item item = clipData.getItemAt(i); @@ -260,6 +262,17 @@ Uri uri = item.getUri(); if (uri == null || !isContentOrFileUri(uri)) continue; + // Reject file URIs and URIs originating from this app to prevent the browser + // from opening private files on behalf of an untrusted drop request. + if (UiAndroidFeatureMap.isEnabled( + UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES)) { + if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme()) + || (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme()) + && ContentUriUtils.isUriFromThisApp(uri))) { + continue; + } + } + String mimeType = getMimeType(context, uri); if (mimeType == null && clipData.getItemCount() == 1 && desc != null) { mimeType = getFallbackMimeType(desc); diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandlerUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandlerUnitTest.java index a198cdf..b55a9234 100644 --- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandlerUnitTest.java +++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandlerUnitTest.java @@ -9,6 +9,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -20,8 +22,13 @@ import android.content.ClipDescription; import android.content.ContentProvider; import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; import android.net.Uri; +import android.os.Process; import android.view.DragAndDropPermissions; import android.view.DragEvent; import android.view.View; @@ -43,10 +50,14 @@ import org.robolectric.shadows.ShadowContentResolver; import org.robolectric.shadows.ShadowMimeTypeMap; +import org.chromium.base.ContextUtils; import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.Features.DisableFeatures; +import org.chromium.base.test.util.Features.EnableFeatures; import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.TabObserver; +import org.chromium.ui.base.UiAndroidFeatures; import org.chromium.ui.base.WindowAndroid; import org.chromium.url.GURL; @@ -55,6 +66,7 @@ /** Unit tests for {@link LocationBarDragDropHandler}. */ @RunWith(BaseRobolectricTestRunner.class) +@DisableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) public class LocationBarDragDropHandlerUnitTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); @@ -71,6 +83,7 @@ @Mock private DragAndDropPermissions mDragAndDropPermissions; @Mock private ContentProvider mContentProvider; @Mock private Intent mIntent; + @Mock private PackageManager mPackageManager; @Captor private ArgumentCaptor<TabObserver> mTabObserverCaptor; @Captor private ArgumentCaptor<OmniboxLoadUrlParams> mLoadUrlParamsCaptor; @@ -92,6 +105,29 @@ shadowMimeTypeMap.addExtensionMimeTypeMapping("pdf", "application/pdf"); } + /** + * Installs an application context whose PackageManager resolves {@code authority} to a provider + * declared by {@code packageName}. + */ + private void setResolvedProviderPackage(String authority, String packageName) { + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.packageName = packageName; + providerInfo.applicationInfo = new ApplicationInfo(); + providerInfo.applicationInfo.uid = + packageName.equals(mContext.getPackageName()) + ? Process.myUid() + : Process.myUid() + 1; + when(mPackageManager.resolveContentProvider(eq(authority), anyInt())) + .thenReturn(providerInfo); + ContextUtils.initApplicationContextForTests( + new ContextWrapper(mContext) { + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + }); + } + @Test public void testOnDrag_DragStarted_ValidMimeType() { when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DRAG_STARTED); @@ -136,6 +172,7 @@ } @Test + @DisableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) public void testOnDrag_Drop_FileUri() { when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); when(mDragEvent.getClipData()).thenReturn(mClipData); @@ -156,6 +193,118 @@ } @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) + public void testOnDrag_Drop_FileUri_DefenseEnabled() { + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(mClipData); + when(mClipData.getItemCount()).thenReturn(1); + when(mClipData.getItemAt(0)).thenReturn(mClipDataItem); + + Uri fileUri = Uri.parse("file:///path/to/file.txt"); + when(mClipDataItem.getUri()).thenReturn(fileUri); + + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + + assertFalse(mHandler.onDrag(new View(mContext), mDragEvent)); + + verify(mOmniboxStub, never()).loadUrl(any()); + } + + @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) + public void testOnDrag_Drop_ContentUriFromThisApp() { + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(mClipData); + when(mClipData.getItemCount()).thenReturn(1); + when(mClipData.getItemAt(0)).thenReturn(mClipDataItem); + + Uri contentUri = Uri.parse("content://com.example.fileprovider/downloads/report.csv"); + when(mClipDataItem.getUri()).thenReturn(contentUri); + + setResolvedProviderPackage("com.example.fileprovider", mContext.getPackageName()); + + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + when(mActivity.requestDragAndDropPermissions(mDragEvent)) + .thenReturn(mDragAndDropPermissions); + + assertFalse(mHandler.onDrag(new View(mContext), mDragEvent)); + + verify(mOmniboxStub, never()).loadUrl(any()); + verify(mTab, never()).addObserver(any()); + verify(mDragAndDropPermissions).release(); + } + + @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) + public void testOnDrag_Drop_UppercaseFileUri_DefenseEnabled() { + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(mClipData); + when(mClipData.getItemCount()).thenReturn(1); + when(mClipData.getItemAt(0)).thenReturn(mClipDataItem); + + Uri fileUri = Uri.parse("FILE:///path/to/file.txt"); + when(mClipDataItem.getUri()).thenReturn(fileUri); + + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + + assertFalse(mHandler.onDrag(new View(mContext), mDragEvent)); + + verify(mOmniboxStub, never()).loadUrl(any()); + } + + @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) + public void testOnDrag_Drop_UppercaseContentUriFromThisApp() { + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(mClipData); + when(mClipData.getItemCount()).thenReturn(1); + when(mClipData.getItemAt(0)).thenReturn(mClipDataItem); + + Uri contentUri = Uri.parse("CONTENT://com.example.fileprovider/downloads/report.csv"); + when(mClipDataItem.getUri()).thenReturn(contentUri); + + setResolvedProviderPackage("com.example.fileprovider", mContext.getPackageName()); + + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + when(mActivity.requestDragAndDropPermissions(mDragEvent)) + .thenReturn(mDragAndDropPermissions); + + assertFalse(mHandler.onDrag(new View(mContext), mDragEvent)); + + verify(mOmniboxStub, never()).loadUrl(any()); + verify(mTab, never()).addObserver(any()); + verify(mDragAndDropPermissions).release(); + } + + @Test + @DisableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) + public void testOnDrag_Drop_ContentUriFromThisApp_DefenseDisabled() { + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(mClipData); + when(mClipData.getItemCount()).thenReturn(1); + when(mClipData.getItemAt(0)).thenReturn(mClipDataItem); + + Uri contentUri = Uri.parse("content://com.example.fileprovider/downloads/report.csv"); + when(mClipDataItem.getUri()).thenReturn(contentUri); + + when(mContentProvider.getType(contentUri)).thenReturn("text/csv"); + ShadowContentResolver.registerProviderInternal( + "com.example.fileprovider", mContentProvider); + + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + when(mActivity.requestDragAndDropPermissions(mDragEvent)) + .thenReturn(mDragAndDropPermissions); + + assertTrue(mHandler.onDrag(new View(mContext), mDragEvent)); + + verify(mOmniboxStub).loadUrl(mLoadUrlParamsCaptor.capture()); + assertEquals( + "content://com.example.fileprovider/downloads/report.csv", + mLoadUrlParamsCaptor.getValue().url); + } + + @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) public void testOnDrag_Drop_ContentUri() { when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); when(mDragEvent.getClipData()).thenReturn(mClipData); @@ -189,6 +338,7 @@ } @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) public void testOnDrag_Drop_ContentUri_LoadFailed() { when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); when(mDragEvent.getClipData()).thenReturn(mClipData); @@ -217,6 +367,7 @@ } @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) public void testOnDrag_Drop_ContentUri_TabDestroyed() { when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); when(mDragEvent.getClipData()).thenReturn(mClipData); @@ -322,6 +473,7 @@ } @Test + @DisableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) public void testFindUriToLoad() { ClipData.Item item1 = mock(ClipData.Item.class); ClipData.Item item2 = mock(ClipData.Item.class); @@ -353,6 +505,25 @@ } @Test + @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES) + public void testFindUriToLoad_SkipsContentUriFromThisApp() { + Uri thisAppUri = Uri.parse("content://com.example.fileprovider/downloads/report.csv"); + Uri otherAppUri = Uri.parse("content://com.example.provider/document.pdf"); + ClipData clipData = + new ClipData( + "clip", + new String[] {"text/csv", "application/pdf"}, + new ClipData.Item(thisAppUri)); + clipData.addItem(new ClipData.Item(otherAppUri)); + + when(mContentProvider.getType(otherAppUri)).thenReturn("application/pdf"); + ShadowContentResolver.registerProviderInternal("com.example.provider", mContentProvider); + setResolvedProviderPackage("com.example.fileprovider", mContext.getPackageName()); + + assertEquals(otherAppUri, mHandler.findUriToLoad(mContext, clipData, mClipDescription)); + } + + @Test public void testOnDrag_Drop_BrowsableIntent() { when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); when(mDragEvent.getClipData()).thenReturn(mClipData); diff --git a/chrome/browser/ui/android/omnibox/BUILD.gn b/chrome/browser/ui/android/omnibox/BUILD.gn index 304569f..baf71bed7 100644 --- a/chrome/browser/ui/android/omnibox/BUILD.gn +++ b/chrome/browser/ui/android/omnibox/BUILD.gn @@ -228,6 +228,7 @@ "$google_play_services_package:google_play_services_location_java", "$google_play_services_package:google_play_services_tasks_java", "//base:base_java", + "//base:content_uri_utils_java", "//base:service_loader_java", "//base:supplier_java", "//base/version_info/android:version_constants_java", @@ -336,6 +337,7 @@ "//third_party/metrics_proto:metrics_proto_java", "//third_party/omnibox_proto:omnibox_proto_java", "//ui/accessibility:ax_base_java", + "//ui/android:ui_android_features_java", "//ui/android:ui_full_java", "//ui/android:ui_no_recycler_view_java", "//ui/android:ui_utils_java", @@ -755,6 +757,7 @@ "//third_party/mockito:mockito_java", "//ui/accessibility:ax_base_java", "//ui/accessibility:ui_accessibility_test_support_java", + "//ui/android:ui_android_features_java", "//ui/android:ui_java_test_support", "//ui/android:ui_junit_test_support", "//ui/android:ui_no_recycler_view_java",
Original Bug Report
Confused-deputy issue in LocationBarDragDropHandler loads private content 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Android Omnibox drag-and-drop handler (LocationBarDragDropHandler) navigates to dropped content:// URIs without verifying whether the URI originates from Chrome itself. A co-installed application in multi-window mode could potentially drag a URI referencing Chrome’s internal FileProvider or persisted Storage Access Framework grants onto the Omnibox. This allows Chrome to open and render private files under its own UID within a tab.
Affected files:
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java
Estimated timestamp from git blame: 2026-07-31
Summary
LocationBarDragDropHandler processes drag-and-drop events targeting Chrome’s Omnibox / URL bar on Android. When a URI is dropped onto the Omnibox, the handler inspects its MIME type and navigates the tab to the dropped URI via OmniboxStub.loadUrl(...) with PageTransition.TYPED. However, the handler lacks confused-deputy validation (such as ContentUriUtils.isUriFromThisApp(uri)). Consequently, a co-installed application without permissions could supply a content:// URI pointing to Chrome’s own ChromeFileProvider or persisted Storage Access Framework grants, forcing Chrome to open and render internal or private files.
Code Analysis
In chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java:96-129:
boolean acceptDrop(Context context, DragEvent event) {
ClipData clipData = event.getClipData();
...
DragAndDropPermissions permissions =
hasContentUri(clipData) ? activity.requestDragAndDropPermissions(event) : null;
try {
Uri uriToLoad = findUriToLoad(context, clipData, event.getClipDescription());
if (uriToLoad == null) return false;
String url = uriToLoad.toString();
...
mOmniboxStub.loadUrl(
new OmniboxLoadUrlParams.Builder(url, PageTransition.TYPED).build());
return true;
} finally { ... }
}
In findUriToLoad (LocationBarDragDropHandler.java:226-255), the handler checks if the item’s MIME type is acceptable (text, image, or PDF) using getMimeType(context, uri) (:161-164), which queries context.getContentResolver().getType(uri). For content:// URIs matching Chrome’s package authority (e.g. content://<package>.FileProvider/...), Android resolves the query using Chrome’s own ChromeFileProvider (org.chromium.chrome.browser.util.ChromeFileProvider), bypassing android:exported="false" due to same-UID execution.
Unlike sibling input vectors in Chrome which explicitly guard against confused-deputy attacks under UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES (e.g., EventForwarder.java:717-725, ClipboardImpl.java:336-344, WebAppLaunchHandler.java:158, and PdfUtils.java:697-710), LocationBarDragDropHandler does not check ContentUriUtils.isUriFromThisApp(uri).
When OmniboxStub.loadUrl navigates to the content:// URI, NavigationURLLoaderImpl creates a ContentURLLoaderFactory (content/browser/loader/navigation_url_loader_impl.cc:2250-2254) with no path or authority restrictions. ContentURLLoader opens the content URI via ContentUriUtils.openContentUri under Chrome’s browser process UID, streaming the unredacted file content directly into the renderer process.
Potential Targets
Per chrome/android/java/res_template/xml/file_paths.xml:12-24, ChromeFileProvider maps paths such as Download/ (downloads), images/, net-export/ (cache), passwords/, pdfs/, and Offline Pages/archives/. For example, files such as Download/ChromePasswords.csv (generated during login DB deprecation export) or cached documents can be addressed and rendered.
Potential Reproduction Steps
(Note: These steps are derived from static analysis; automated runtime PoC execution was not performed.)
- Install a zero-permission application on an Android device running Chrome in split-screen / multi-window mode.
- In the attacker application, initiate a global drag (
View.startDragAndDrop) with aClipDatacontaining aUripointing tocontent://<chrome_package>.FileProvider/downloads/ChromePasswords.csvand aClipDescriptiondeclaringtext/plain. - Drag the payload and release it over Chrome’s Omnibox / URL bar.
LocationBarDragDropHandler.acceptDropresolves the MIME type viaChromeFileProviderastext/csvand issues aloadUrlnavigation withPageTransition.TYPED.- Chrome’s browser process loads the file and renders its raw contents in the tab.
Suggested Fix
In LocationBarDragDropHandler.findUriToLoad (or in acceptDrop), reject any content:// URI where ContentUriUtils.isUriFromThisApp(uri) is true under the UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES feature gate, mirroring the defense in EventForwarder.java:717-725.
Evaluated with Chrome root at commit: 53c437ac4ea52cc89d0265eb9e6d68666fdb303e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.