Low watch CVSS 5.4 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Downloads
DescriptionInsufficient validation of untrusted input in Downloads
ComponentDownloads
Sub-componentUI>Browser>Downloads
Bug ClassInput Validation
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.
Tracker421511847
Fix commit81de6a2b6cee (chromium/src) +29/-10
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedAbhishek Kumar
Reported2025-05-31 · bounty $5000 (per vendor advisory)
Fix landed2025-07-07 (public git)
Disclosed2025-09-02
Patch gap57 days — fix public in source before the advisory

Background

`DownloadUtils.formatUrlForDisplayInNotification`
an Android helper that turns a download’s source URL into a short string for Download Home captions, the download-complete message and download notifications, subject to a character limit.
`UrlFormatter.formatUrlForSecurityDisplay`
formats a URL for display in a security-relevant UI, here with SchemeDisplay.OMIT_HTTP_AND_HTTPS.
eTLD+1 (`UrlUtilities.getDomainAndRegistry`)
the registrable domain (for example example.com), used as a shorter fallback; it is only meaningful for URLs that have a host.
Hostless URLs
URLs whose scheme or origin has no host part, so trying to extract a registrable domain from their spec does not identify a real site.

Root Cause Analysis

formatUrlForDisplayInNotification returned the security-formatted URL whenever its length was within limit, including when it was empty. Otherwise it fell back to UrlUtilities.getDomainAndRegistry(url.getSpec(), false) without checking that the URL had a host. As the commit explains, that fallback is wrong for URLs without a host (some schemes): eTLD+1 parsing of the full spec can produce a string that does not represent the download’s real source, so the download UI showed a misleading origin. The broken rule is that a shortened origin must never claim more than the actual origin supports.

The fix requires a non-empty formatted URL before returning it. It then derives origin = url.getOrigin(), calls getDomainAndRegistry(origin.getSpec(), true) only when the origin is valid and origin.getHost() is non-empty (otherwise it uses origin.getPossiblyInvalidSpec()), and returns null if that fallback is empty or over limit. Callers in DownloadMessageUiControllerImpl and UiUtils now omit the origin text on null/empty instead of inserting a bad value. This tightens the validation.

Key insight
The eTLD+1 fallback ran on the raw URL spec without checking that a host existed, so hostless URLs could produce misleading “domain” strings in trusted download UI. The fix parses eTLD+1 only from an origin that has a host, and otherwise shows no origin at all.

Attack Path

  1. Attacker triggers a download A malicious page starts a download on Chrome for Android whose URL has no host (the commit says “certain URL schemes”; which schemes is not shown) and whose formatted form is longer than the display limit.
  2. Fallback chosen formatUrlForSecurityDisplay returns a string longer than MAX_ORIGIN_LENGTH_FOR_NOTIFICATION or MAX_ORIGIN_LENGTH_FOR_DOWNLOAD_HOME_CAPTION, so the old code took the eTLD+1 fallback on url.getSpec().
  3. Misleading origin rendered The “domain” parsed from the hostless spec is shown in the download-complete message, the Download Home list caption or the notification as if it were the file’s source.
  4. User trusts spoofed source (inferred) The user may believe the file came from a trusted site and open it (inferred).

Impact Assessment

This is a UI spoofing issue in the Android browser process’s download surfaces. An attacker can affect the origin text attributed to a downloaded file; there is no memory-safety impact. Ordinary web content can reach it in a stock Android build by starting a download from a suitable hostless URL. No compromised renderer, flag or extension is needed, though the user has to view the download UI and act on the file for there to be practical harm. The vendor rates this low (“Insufficient validation of untrusted input in Downloads”). The provided metadata does not say whether it was exploited in the wild.

Changed Functions

FunctionChangeNotes
if
chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.java
modified

Files Changed

  • chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl.java
  • chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.java
  • components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java

Audit Directions

  • eTLD+1 on hostless or opaque URLs
    Search for getDomainAndRegistry (Java and C++) called on getSpec() or untrusted URLs without first checking for a host or a non-opaque origin, especially in permission prompts, notifications and download UI.
  • Length-based fallbacks in security UI
    Review code that swaps in a “shortened” origin when the full one is too long. Make sure the shortened form is derived from the real origin and fails closed (omit it, or elide safely) rather than inventing a plausible-looking domain.
  • Null/empty origin propagation
    Check callers of URL-display helpers for format strings that will print null, empty strings or attacker-controlled fallbacks into trusted browser UI.
From 81de6a2b6cee7bce14a1586def1d782d9c16e618 Mon Sep 17 00:00:00 2001
From: Lily Chen <[email protected]>
Date: Mon, 07 Jul 2025 13:16:21 -0700
Subject: [PATCH] Android downloads UI: Only parse URL for eTLD+1 if it has a host

This CL fixes a helper function used in downloads UI on Android, which
formats a URL for display in the Download Home list, download completion
message, and download notification. Previously, the function would fall
back to an eTLD+1 from getDomainAndRegistry() if the formatted URL was
too long for the display surface. This is subtly incorrect for URLs
without a host (e.g. certain URL schemes), and can result in misleading
UI strings displayed to the user.

In this CL, we add a check that the origin has a host, and we omit the
URL/domain from the UI display if we cannot get a suitable formatted
URL/eTLD+1.

Screenshots:
https://drive.google.com/drive/folders/1M7lElUPRMstiM_zfgzsYVU8x0PQQMdD-

Bug: 421511847
Change-Id: Id4e48441d30427a6d73ab74f69caf6a778c49a99
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6701188
Reviewed-by: Theresa Sullivan <[email protected]>
Commit-Queue: Lily Chen <[email protected]>
Reviewed-by: Xinghui Lu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1483331}
---

diff --git a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl.java b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl.java
index a25366f..d76ac4b2 100644
--- a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl.java
+++ b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl.java
@@ -797,6 +797,7 @@
                 String bytesString =
                         org.chromium.components.browser_ui.util.DownloadUtils.getStringForBytes(
                                 getContext(), itemToShow.totalSizeBytes);
+                // Try to display the download domain/origin if possible. Otherwise, omit it.
                 String displayUrl =
                         DownloadUtils.formatUrlForDisplayInNotification(
                                 itemToShow.url, DownloadUtils.MAX_ORIGIN_LENGTH_FOR_NOTIFICATION);
@@ -805,7 +806,7 @@
                                 .getString(
                                         R.string.download_message_download_complete_description,
                                         bytesString,
-                                        displayUrl);
+                                        displayUrl != null ? displayUrl : "");
                 info.id = itemToShow.id;
                 info.link = getContext().getString(R.string.open_downloaded_label);
                 info.icon = R.drawable.infobar_download_complete_animation;
diff --git a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.java b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.java
index ab1513d9..76c3a605 100644
--- a/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.java
+++ b/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.java
@@ -8,6 +8,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.text.format.Formatter;
 
@@ -147,15 +148,20 @@
         String displayUrl =
                 DownloadUtils.formatUrlForDisplayInNotification(
                         item.url, DownloadUtils.MAX_ORIGIN_LENGTH_FOR_DOWNLOAD_HOME_CAPTION);
+        boolean hasDisplayUrl = !TextUtils.isEmpty(displayUrl);
 
         if (item.totalSizeBytes == 0) {
-            return context.getString(
-                    R.string.download_manager_list_item_description_no_size, displayUrl);
+            return hasDisplayUrl
+                    ? context.getString(
+                            R.string.download_manager_list_item_description_no_size, displayUrl)
+                    : "";
         }
 
         String displaySize = Formatter.formatFileSize(context, item.totalSizeBytes);
-        return context.getString(
-                R.string.download_manager_list_item_description, displaySize, displayUrl);
+        return hasDisplayUrl
+                ? context.getString(
+                        R.string.download_manager_list_item_description, displaySize, displayUrl)
+                : displaySize;
     }
 
     /**
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 adce708..5224900 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
@@ -137,7 +137,7 @@
      *
      * @param url The full URL.
      * @param limit Character limit.
-     * @return The text to display, or null if the input was invalid.
+     * @return The text to display, or null if the input was invalid or cannot be shortened enough.
      */
     public static @Nullable String formatUrlForDisplayInNotification(
             @Nullable GURL url, int limit) {
@@ -145,11 +145,23 @@
 
         String formattedUrl =
                 UrlFormatter.formatUrlForSecurityDisplay(url, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
-        if (formattedUrl.length() <= limit) return formattedUrl;
+        if (!TextUtils.isEmpty(formattedUrl) && formattedUrl.length() <= limit) {
+            return formattedUrl;
+        }
 
-        // The origin is too long. Strip down to eTLD+1.
-        return UrlUtilities.getDomainAndRegistry(
-                url.getSpec(), /* includePrivateRegistries= */ false);
+        // 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();
+        String fallback =
+                !GURL.isEmptyOrInvalid(origin) && !origin.getHost().isEmpty()
+                        ? UrlUtilities.getDomainAndRegistry(
+                                origin.getSpec(), /* includePrivateRegistries= */ true)
+                        : origin.getPossiblyInvalidSpec();
+        if (!TextUtils.isEmpty(fallback) && fallback.length() <= limit) {
+            return fallback;
+        }
+        return null;
     }
 
     /**
Loading diff…

Original Bug Report

reported by [email protected]

Download origin spoofing using malformed data url.

Steps to reproduce the problem

  1. Enter this payload in the address bar on android chrome: data://google.com/html,somereallyreallylongtextattheaddressbar and press enter, you will notice a download begins with origin in the notification as google.com, same origin is displayed in chrome://downloads menu too.

Full potential exploitation of this vulnerability: Download an apk from google.com as spoofed origin: Live POC: https://gojo-satorou-v7.github.io/chigorin/

Host below 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>

  <script>
    // Listen to any keydown
    window.addEventListener('keydown', function(e) {
      // open the download
      openWin();
      // request fullscreen
      //goFullScreen();
    });

    // 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://google.com/application/x-msdownload;base64,ZXN0dGVzdGVzdHRlc3Q=",
        "google.apk"
      );
    }

  </script>
  

  
</body>
</html>

Press space [since I am on emulator] Notice an apk will be downloaded from google.com

Problem Description

Root cause analysis:

Let’s go step by step:

  1. Why is the above payload triggering a download?

Because of this code.

    bool is_download;
    bool must_download =
        download_utils::MustDownload(url_, head.headers.get(), head.mime_type);
    bool known_mime_type = blink::IsSupportedMimeType(head.mime_type);
#if BUILDFLAG(ENABLE_PLUGINS)
    if (!head.intercepted_by_plugin && !must_download && !known_mime_type) {
      // No plugin throttles intercepted the response. Ask if the plugin
      // registered to PluginService wants to handle the request.
      CheckPluginAndContinueOnReceiveResponse(
          head, std::move(url_loader_client_endpoints),
          true /* is_download_if_not_handled_by_plugin */,
          std::vector<WebPluginInfo>());
      return;
    }

https://github.com/chromium/chromium/blob/23df03bf4570c5b17d918be5daf785d60ba0707b/content/browser/loader/navigation_url_loader_impl.cc#L1178

The format of a data uri is as follows: data:[<mime-type>][;base64],<data> and according to above code if the mime type is unknown it will trigger a download.

  1. Chromium’s GURL sees the scheme data and, because of the //, treats google.com as a host. Then url.GetContent() returns the substring after the scheme (which in this case includes the path /html,sometext). DataURL::Parse() splits at the first comma: the part before the comma is interpreted as “media type” metadata, and the part after as the raw data. In our example:

    Content before comma = “google.com/html” (misinterpreted as MIME/media type)

    Content after comma = “sometext” (the actual payload)

Because “google.com/html” is not a valid MIME type, the parser falls back to defaults.

So now we know why the malformed data url is causing a download, next I will show you why is the download notification taking google.com as origin (seems obvious now, GURL is the culprit!)

  1. How does the download notification handling the data uri?

In the in-app download notification (via DownloadMessageUiControllerImpl), the code calls DownloadUtils.formatUrlForDisplayInNotification with the download’s URL. That method (in components/browser_ui/util/android/DownloadUtils.java) does:

if (GURL.isEmptyOrInvalid(url)) return null;
String formattedUrl = UrlFormatter.formatUrlForSecurityDisplay(
    url, UrlFormatter.SchemeDisplay.OMIT_HTTP_AND_HTTPS);
if (formattedUrl.length() <= MAX_ORIGIN_LENGTH) return formattedUrl;
// Too long – strip to eTLD+1
return UrlUtilities.getDomainAndRegistry(url.getSpec(), false);

For a malformed data URL like data://google.com/html,sometext, UrlFormatter.formatUrlForSecurityDisplay(url) will include the scheme and full URL by default (since the scheme is “data”, not HTTP/HTTPS, it is not omitted). The string “data://google.com/html,sometext” exceeds the MAX_ORIGIN_LENGTH (25 chars), so the code falls back to UrlUtilities.getDomainAndRegistry(url.getSpec()), which extracts the effective top-level domain+1 from the full URL string. In this case it returns “google.com”. That string is then shown as the “origin” in the notification.

Additional Comments

Oldest version in which this POC works: 130.0.5723.40 Stable works till current latest version of 136.0.7103.127

Please note: I was only able to get the above version apk of chrome from a third party website, although I am highly confident that the vulnerability started existing after the release of 130.x.x.x Oldest version in which this vulnerability doesn’t work: 129.x.x.x

Summary

Download origin spoofing using malformed data url.

Additional Data

Category: Security
Chrome Channel: Stable
Regression: No

View on issue tracker