CVE-2025-12908
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/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.javachrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/UiUtils.javacomponents/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DownloadUtils.java
Patch
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; } /**
Original Bug Report
Download origin spoofing using malformed data url.
Steps to reproduce the problem
- Enter this payload in the address bar on android chrome:
data://google.com/html,somereallyreallylongtextattheaddressbarand 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:
- 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;
}
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.
-
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!)
- 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