Medium watch CVSS 5.4 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Downloads
DescriptionInappropriate implementation in Downloads
ComponentDownloads
Sub-componentUI>Browser>Downloads
Bug ClassLogic Error
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.
Tracker415496161
Fix commitb2bf216dc223 (chromium/src) +102/-15
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedFarras Givari
Reported2025-05-04 · bounty $1000 (per vendor advisory)
Fix landed2025-07-02 (public git)
Disclosed2025-09-02
Patch gap62 days — fix public in source before the advisory

Background

`DangerousDownloadDialog`
the Android modal dialog (“File might be harmful”) that asks the user to confirm a download Chrome has flagged as dangerous, with a “Download anyway” positive button.
Site-controlled filename
the fileName shown in the dialog comes from download_item->GetFileNameToReportUser(), which a website can influence (for example through the response’s suggested filename), so it is untrusted text inside a browser security prompt.
`TextUtils.expandTemplate`
the Android helper that replaces ^1, ^2, … placeholders with CharSequence values while keeping spans such as bold or color, which the patch uses instead of String.format-style %1$s placeholders.
`FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains`
a url_formatter routine that reduces a URL to a display domain; the patch uses it on item->GetURL() to show where the download comes from.

Root Cause Analysis

Before the patch, DangerousDownloadDialog.show built its message as plain text: Do you want to download %1$s anyway? (or the _WITH_SIZE variant), with the site-controlled filename inserted unquoted and with the same styling as the Chrome-authored text around it, and with no indication of which site the file came from. A site could therefore pick a filename that reads like part of Chrome’s own sentence, or that hides the dangerous extension, and the user had no origin to judge the request by. The removed code also had the size condition backwards: when totalBytes > 0 it used the no-size string, and it used ..._TEXT_WITH_SIZE only when the size was unknown.

The fix adds getMessageParagraphs, which wraps the filename in quotation marks (in all four .grd strings) and in a StyleSpan(Typeface.BOLD), adds the download domain from the new native helper GetDownloadDomain in a link-colored ForegroundColorSpan, and selects among four templates using correct hasSize/hasDownloadDomain logic. These changes separate untrusted content from Chrome’s text and add origin information; the patch adds no new validation of the filename itself.

Key insight
The dangerous-download warning inserted an attacker-influenced filename into a security prompt with no visual separation and no source origin, so the prompt’s meaning could be manipulated by the site. The fix delimits the filename (quotes and bold) and adds the download URL’s domain, so users can see what is site-controlled and where the file comes from.

Attack Path

  1. Serve a flagged file with a crafted name A malicious site starts a download that Chrome on Android classifies as dangerous, choosing a filename meant to blend into the surrounding warning text or to disguise the file type (the exact spoofing strings are not in the diff; inferred).
  2. Dialog renders untrusted text inline DangerousDownloadDialogBridge passes GetFileNameToReportUser() to DangerousDownloadDialog.show, which before the patch substituted it into IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT with no quotes or styling and without any domain.
  3. User misreads the warning With no clear boundary between the site’s text and Chrome’s text, and no “from <domain>” context, the user may believe the file is benign or comes from a trusted source (inferred).
  4. User confirms The user taps “Download anyway”, and the dangerous file is saved to the device. Whether it is then opened or run depends on later user actions (inferred).

Impact Assessment

A web attacker can make Chrome for Android’s dangerous-download confirmation misleading, which makes it more likely that a user accepts a file Chrome had already flagged as harmful. This is UI spoofing in browser chrome, not a memory-safety issue, and it needs the user to tap through the dialog. The advisory rates it as medium (“Inappropriate implementation in Downloads”) and does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
DangerousDownloadDialogBridge
chrome/browser/download/android/dangerous_download_dialog_bridge.cc
modified
if
chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DangerousDownloadDialogBridge.java
modified
if
chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DangerousDownloadDialog.java
modified

Files Changed

  • chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogIncognitoTest.java
  • chrome/browser/download/android/dangerous_download_dialog_bridge.cc
  • chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DangerousDownloadDialogBridge.java
  • chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DangerousDownloadDialog.java
  • chrome/browser/ui/android/strings/android_chrome_strings.grd
  • chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT.png.sha1
  • chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_DOMAIN.png.sha1
  • chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE.png.sha1
  • chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE_AND_DOMAIN.png.sha1

Audit Directions

  • Untrusted strings in security prompts
    Look at other permission, download, external-protocol and file-picker dialogs (on all platforms) that insert filenames, page titles or URL fragments into a sentence without delimiters, for possible spoofing of Chrome-authored text, including bidirectional-text and newline characters.
  • Origin attribution
    Check whether consequential prompts display the right origin: GetDownloadDomain uses item->GetURL(), so check how this behaves for blob:/data: URLs, redirects and downloads started from a different initiator (an empty domain falls back to the no-domain template).
  • Template and condition errors
    The removed code chose the size template backwards (totalBytes > 0 selected the no-size string); look for similar inverted ternaries in other prompt-building code where the wrong template can hide security-relevant details.
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogIncognitoTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogIncognitoTest.java
index 7c59973..41667efc 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogIncognitoTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadDialogIncognitoTest.java
@@ -44,6 +44,7 @@
     private static final String DOWNLOAD_PATH = "/android/Download";
     private static final String PAGE_URL = "www.pageurl.com/download";
     private static final String FILE_NAME = "download.pdf";
+    private static final String DOWNLOAD_DOMAIN = "pageurl.com";
     public static final int ICON_ID = R.drawable.btn_close;
 
     @Rule
@@ -179,6 +180,7 @@
                                     mModalDialogManager,
                                     FILE_NAME,
                                     TOTAL_BYTES,
+                                    DOWNLOAD_DOMAIN,
                                     ICON_ID,
                                     mResultCallback);
                 });
diff --git a/chrome/browser/download/android/dangerous_download_dialog_bridge.cc b/chrome/browser/download/android/dangerous_download_dialog_bridge.cc
index 100df18c..7039954 100644
--- a/chrome/browser/download/android/dangerous_download_dialog_bridge.cc
+++ b/chrome/browser/download/android/dangerous_download_dialog_bridge.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/android/resource_mapper.h"
 #include "chrome/browser/download/android/download_dialog_utils.h"
 #include "chrome/grit/generated_resources.h"
+#include "components/url_formatter/elide_url.h"
 #include "ui/android/window_android.h"
 #include "ui/base/l10n/l10n_util.h"
 
@@ -25,6 +26,15 @@
 using base::android::ConvertJavaStringToUTF8;
 using base::android::JavaParamRef;
 
+namespace {
+// Gets the "download domain" string shown in the dialog. Currently, this is
+// derived from the download URL.
+std::u16string GetDownloadDomain(download::DownloadItem* item) {
+  return url_formatter::FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains(
+      item->GetURL());
+}
+}  // namespace
+
 DangerousDownloadDialogBridge::DangerousDownloadDialogBridge() {
   JNIEnv* env = base::android::AttachCurrentThread();
   java_object_.Reset(Java_DangerousDownloadDialogBridge_create(
@@ -59,6 +69,8 @@
       download_item->GetGuid(),
       base::UTF8ToUTF16(download_item->GetFileNameToReportUser().value()),
       download_item->GetTotalBytes(),
+      base::android::ConvertUTF16ToJavaString(env,
+                                              GetDownloadDomain(download_item)),
       ResourceMapper::MapToJavaDrawableId(IDR_ANDROID_INFOBAR_WARNING));
 }
 
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DangerousDownloadDialogBridge.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DangerousDownloadDialogBridge.java
index 350a718..dbaabb4a 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DangerousDownloadDialogBridge.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DangerousDownloadDialogBridge.java
@@ -45,6 +45,7 @@
      * @param guid GUID of the download.
      * @param fileName Name of the download file.
      * @param totalBytes Total bytes of the file.
+     * @param downloadDomain Domain name to associate with the downloaded file.
      * @param iconId The icon resource for the warning dialog.
      */
     @CalledByNative
@@ -53,6 +54,7 @@
             @JniType("std::string") String guid,
             @JniType("std::u16string") String fileName,
             long totalBytes,
+            String downloadDomain,
             int iconId) {
         Activity activity = windowAndroid.getActivity().get();
         if (activity == null) {
@@ -66,6 +68,7 @@
                         ((ModalDialogManagerHolder) activity).getModalDialogManager(),
                         fileName,
                         totalBytes,
+                        downloadDomain,
                         iconId,
                         (accepted) -> {
                             if (accepted) {
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DangerousDownloadDialog.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DangerousDownloadDialog.java
index 0c31593..34eb503 100644
--- a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DangerousDownloadDialog.java
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/DangerousDownloadDialog.java
@@ -5,6 +5,12 @@
 package org.chromium.chrome.browser.download.dialogs;
 
 import android.content.Context;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
 
 import androidx.annotation.IntDef;
 import androidx.core.content.res.ResourcesCompat;
@@ -13,6 +19,7 @@
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.browser.download.R;
+import org.chromium.components.browser_ui.styles.SemanticColorUtils;
 import org.chromium.components.browser_ui.util.DownloadUtils;
 import org.chromium.ui.UiUtils;
 import org.chromium.ui.modaldialog.DialogDismissalCause;
@@ -55,31 +62,25 @@
 
     /**
      * Called to show a warning dialog for dangerous download.
+     *
      * @param context Context for showing the dialog.
      * @param modalDialogManager Manager for managing the modal dialog.
      * @param fileName Name of the download file.
      * @param totalBytes Total bytes of the file.
+     * @param downloadDomain Domain name to associate with the downloaded file.
      * @param iconId Icon ID of the warning dialog.
      * @param callback Callback to run when confirming the download, true for accept the download,
-     *         false otherwise.
+     *     false otherwise.
      */
     public void show(
             Context context,
             ModalDialogManager modalDialogManager,
             String fileName,
             long totalBytes,
+            String downloadDomain,
             int iconId,
             Callback<Boolean> callback) {
         var resources = context.getResources();
-        String message =
-                totalBytes > 0
-                        ? resources.getString(R.string.dangerous_download_dialog_text, fileName)
-                        : resources.getString(
-                                R.string.dangerous_download_dialog_text_with_size,
-                                fileName,
-                                DownloadUtils.getStringForBytes(context, totalBytes));
-        ArrayList<CharSequence> message_paragraphs = new ArrayList<>(List.of(message));
-
         var controller =
                 new ModalDialogProperties.Controller() {
                     @Override
@@ -112,13 +113,16 @@
                         }
                     }
                 };
+
         PropertyModel propertyModel =
                 new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                         .with(ModalDialogProperties.CONTROLLER, controller)
                         .with(
                                 ModalDialogProperties.TITLE,
                                 resources.getString(R.string.dangerous_download_dialog_title))
-                        .with(ModalDialogProperties.MESSAGE_PARAGRAPHS, message_paragraphs)
+                        .with(
+                                ModalDialogProperties.MESSAGE_PARAGRAPHS,
+                                getMessageParagraphs(context, fileName, totalBytes, downloadDomain))
                         .with(
                                 ModalDialogProperties.POSITIVE_BUTTON_TEXT,
                                 resources.getString(
@@ -143,6 +147,64 @@
     }
 
     /**
+     * Selects the appropriate message string template and inserts formatted substitutions.
+     *
+     * @return an ArrayList suitable for setting as MESSAGE_PARAGRAPHS property for the dialog.
+     */
+    private static ArrayList<CharSequence> getMessageParagraphs(
+            Context context, String fileName, long totalBytes, String downloadDomain) {
+        boolean hasSize = totalBytes > 0;
+        boolean hasDownloadDomain = !downloadDomain.isEmpty();
+
+        int stringResId;
+        int numSubstitutions;
+        if (hasSize && hasDownloadDomain) {
+            stringResId = R.string.dangerous_download_dialog_text_with_size_and_domain;
+            numSubstitutions = 3;
+        } else if (hasSize) {
+            stringResId = R.string.dangerous_download_dialog_text_with_size;
+            numSubstitutions = 2;
+        } else if (hasDownloadDomain) {
+            stringResId = R.string.dangerous_download_dialog_text_with_domain;
+            numSubstitutions = 2;
+        } else {
+            stringResId = R.string.dangerous_download_dialog_text;
+            numSubstitutions = 1;
+        }
+        CharSequence messageTemplate = context.getString(stringResId);
+        ArrayList<CharSequence> substitutions = new ArrayList<CharSequence>();
+
+        SpannableString formattedFileName = new SpannableString(fileName);
+        formattedFileName.setSpan(
+                new StyleSpan(Typeface.BOLD),
+                0,
+                fileName.length(),
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        substitutions.add(formattedFileName);
+
+        if (hasSize) {
+            String formattedSize = DownloadUtils.getStringForBytes(context, totalBytes);
+            substitutions.add(formattedSize);
+        }
+
+        if (hasDownloadDomain) {
+            SpannableString formattedDownloadDomain = new SpannableString(downloadDomain);
+            formattedDownloadDomain.setSpan(
+                    new ForegroundColorSpan(SemanticColorUtils.getDefaultTextColorLink(context)),
+                    0,
+                    downloadDomain.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            substitutions.add(formattedDownloadDomain);
+        }
+
+        assert substitutions.size() == numSubstitutions;
+        CharSequence message =
+                TextUtils.expandTemplate(
+                        messageTemplate, substitutions.toArray(new CharSequence[0]));
+        return new ArrayList<>(List.of(message));
+    }
+
+    /**
      * Collects dangerous download dialog UI event metrics.
      *
      * @param event The UI event to collect.
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 95db4b7..4ecec15 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -2200,10 +2200,16 @@
         Download anyway
       </message>
       <message name="IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT" desc="Warning message shown on a dialog to ask user to validate the download of a dangerous file.">
-        Do you want to download <ph name="FILE_NAME">%1$s<ex>malware.exe</ex></ph> anyway?
+        Do you want to download "<ph name="FILE_NAME">^1<ex>malware.exe</ex></ph>" anyway?
+      </message>
+      <message name="IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_DOMAIN" desc="Warning message shown on a dialog to ask user to validate the download of a dangerous file, including the domain of the download.">
+        Do you want to download "<ph name="FILE_NAME">^1<ex>malware.exe</ex></ph>" from <ph name="DOWNLOAD_DOMAIN">^2<ex>badsite.com</ex></ph> anyway?
       </message>
       <message name="IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE" desc="Warning message shown on a dialog to ask user to validate the download of a dangerous file, with size information.">
-        Do you want to download <ph name="FILE_NAME">%1$s<ex>malware.exe</ex></ph> (<ph name="FILE_SIZE">%2$s<ex>100MB</ex></ph>) anyway?
+        Do you want to download "<ph name="FILE_NAME">^1<ex>malware.exe</ex></ph>" (<ph name="FILE_SIZE">^2<ex>100MB</ex></ph>) anyway?
+      </message>
+      <message name="IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE_AND_DOMAIN" desc="Warning message shown on a dialog to ask user to validate the download of a dangerous file, with size information and the domain of the download.">
+        Do you want to download "<ph name="FILE_NAME">^1<ex>malware.exe</ex></ph>" (<ph name="FILE_SIZE">^2<ex>100MB</ex></ph>) from <ph name="DOWNLOAD_DOMAIN">^3<ex>badsite.com</ex></ph> anyway?
       </message>
       <message name="IDS_DANGEROUS_DOWNLOAD_DIALOG_TITLE" desc="Title of the dialog for asking user to confirm a dangerous download">
         File might be harmful
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT.png.sha1
index 743476d..72bfbc0 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT.png.sha1
@@ -1 +1 @@
-0030062f1ca249adc2da7b55e5b100df323182fe
\ No newline at end of file
+560efd7c65ac68e98631a3e9b8556f6989b3e599
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_DOMAIN.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_DOMAIN.png.sha1
new file mode 100644
index 0000000..54d30020
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_DOMAIN.png.sha1
@@ -0,0 +1 @@
+157768c9a23ead635a19d3391587220293397e69
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE.png.sha1
index 743476d..03f0040 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE.png.sha1
@@ -1 +1 @@
-0030062f1ca249adc2da7b55e5b100df323182fe
\ No newline at end of file
+b2bdbe28d07a836efadad9fa6182a67525afa58d
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE_AND_DOMAIN.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE_AND_DOMAIN.png.sha1
new file mode 100644
index 0000000..f8c89d08
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_DANGEROUS_DOWNLOAD_DIALOG_TEXT_WITH_SIZE_AND_DOMAIN.png.sha1
@@ -0,0 +1 @@
+d7a03d5ac212f99244db8e3c62edf0de5b8e4af4
\ No newline at end of file
Loading diff…

Original Bug Report

reported by fr...@gmail.com

"File might be harmful" dialog does not have origin

Steps to reproduce the problem

  1. Access poc.html
  2. Click the button
  3. The download pop-up will still appear on https://google.com

Problem Description

This vulnerability occurs when there is a condition that a file download pop-up origin can be overlaid on other origin. The file download pop-up should only be appear when user is accessing the real origin where the file download pop-up come from.

For more information you can read on crbug.com/40055527

Chrome version: 135.0.7049.111 (Official Build) (64-bit)

Summary

crbug.com/40055527 is still reproducible on newest version of Chrome

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: Yes

View on issue tracker