Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Downloads
DescriptionInsufficient validation of untrusted input in Downloads
ComponentDownloads
Bug ClassLogic Error
Tracker421511847
Fix commit81de6a2b6cee (chromium/src) +29/-10
CISA KEVNot listed
CreditedAbhishek Kumar
Disclosed2025-09-02

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
From 81de6a2b6cee7bce14a1586def1d782d9c16e618 Mon Sep 17 00:00:00 2001
From: Lily Chen <chlily@chromium.org>
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 <twellington@chromium.org>
Commit-Queue: Lily Chen <chlily@chromium.org>
Reviewed-by: Xinghui Lu <xinghuilu@chromium.org>
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 x4...@gmail.com

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