Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Downloads
DescriptionIncorrect security UI in Downloads
ComponentDownloads
Bug ClassLogic Error
Tracker419921726
Fix commit97ae0a483361 (chromium/src) +215/-197
CISA KEVNot listed
CreditedFarras Givari
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
if
chrome/browser/resources/downloads/item.ts
modified

Files Changed

  • chrome/app/generated_resources.grd
  • chrome/app/generated_resources_grd/IDS_DOWNLOADS_TOAST_COPIED_LINK.png.sha1
  • chrome/browser/resources/downloads/item.ts
  • chrome/browser/ui/webui/downloads/downloads_ui.cc
  • chrome/test/data/webui/downloads/item_test.ts
From 97ae0a483361b3253153191df0653e928ad0d8ec Mon Sep 17 00:00:00 2001
From: Andrew Liu <[email protected]>
Date: Thu, 19 Feb 2026 10:12:37 -0800
Subject: [PATCH] Show generic toast when copying data URIs from the downloads page

Bug: 419921726
Change-Id: Icdcc171e7b7739473924f67362aa5bcc6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7589699
Reviewed-by: Lily Chen <[email protected]>
Commit-Queue: Andrew Liu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1587252}
---

diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 21b08517..3602002 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -2385,6 +2385,10 @@
                desc="On the chrome://downloads page, the text for the toast shown when the user tried to copy a download's url but the operation failed.">
         Failed to copy link
       </message>
+      <message name="IDS_DOWNLOADS_TOAST_COPIED_LINK"
+               desc="On the chrome://downloads page, the text for the toast shown when the user copies a download's url.">
+        Link copied to clipboard
+      </message>
 
       <message name="IDS_EMAIL_VERIFIED" desc="Toast message to notify the user when their email is automatically verified by the browser.">
         Email automaticaly verified
diff --git a/chrome/app/generated_resources_grd/IDS_DOWNLOADS_TOAST_COPIED_LINK.png.sha1 b/chrome/app/generated_resources_grd/IDS_DOWNLOADS_TOAST_COPIED_LINK.png.sha1
new file mode 100644
index 0000000..db97df6
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_DOWNLOADS_TOAST_COPIED_LINK.png.sha1
@@ -0,0 +1 @@
+8728956ea5e1cbf3f2617e4f8ed4dd082193782b
\ No newline at end of file
diff --git a/chrome/browser/resources/downloads/item.ts b/chrome/browser/resources/downloads/item.ts
index 938f0937..6d5f84c 100644
--- a/chrome/browser/resources/downloads/item.ts
+++ b/chrome/browser/resources/downloads/item.ts
@@ -1206,10 +1206,19 @@
       return;
     }
     if (copied) {
-      const pieces = loadTimeData.getSubstitutedStringPieces(
-                         loadTimeData.getString('toastCopiedDownloadLink'),
-                         this.data.url) as unknown as
-          Array<{collapsible: boolean, value: string, arg: string}>;
+      let pieces;
+      if (this.data.url.startsWith('data:')) {
+        pieces = [{
+          collapsible: false,
+          value: loadTimeData.getString('toastCopiedLink'),
+          arg: '',
+        }];
+      } else {
+        pieces = loadTimeData.getSubstitutedStringPieces(
+                     loadTimeData.getString('toastCopiedDownloadLink'),
+                     this.data.url) as unknown as
+            Array<{collapsible: boolean, value: string, arg: string}>;
+      }
       pieces.forEach(p => {
         p.collapsible = !!p.arg;
       });
diff --git a/chrome/browser/ui/webui/downloads/downloads_ui.cc b/chrome/browser/ui/webui/downloads/downloads_ui.cc
index 6bd0424..d9bb304 100644
--- a/chrome/browser/ui/webui/downloads/downloads_ui.cc
+++ b/chrome/browser/ui/webui/downloads/downloads_ui.cc
@@ -131,6 +131,7 @@
        IDS_DOWNLOADS_TOAST_DELETED_FROM_HISTORY_STILL_ON_DEVICE},
       {"toastDeletedFromHistory", IDS_DOWNLOADS_TOAST_DELETED_FROM_HISTORY},
       {"toastCopiedDownloadLink", IDS_DOWNLOADS_TOAST_COPIED_DOWNLOAD_LINK},
+      {"toastCopiedLink", IDS_DOWNLOADS_TOAST_COPIED_LINK},
       {"toastCopyDownloadLinkFailed",
        IDS_DOWNLOADS_TOAST_COPY_DOWNLOAD_LINK_FAILED},
       {"undo", IDS_DOWNLOAD_UNDO},
diff --git a/chrome/test/data/webui/downloads/item_test.ts b/chrome/test/data/webui/downloads/item_test.ts
index 8ebf725..d34f614 100644
--- a/chrome/test/data/webui/downloads/item_test.ts
+++ b/chrome/test/data/webui/downloads/item_test.ts
@@ -111,227 +111,209 @@
     assertTrue(item.getFileIcon().hidden);
   });
 
-  test(
-      'icon overridden by display type', async () => {
-        testIconLoader.setShouldIconsLoad(true);
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kSensitiveContentBlock,
-        });
-        await microtasksFinished();
-        assertEquals(
-            'cr:error', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'red',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+  test('icon overridden by display type', async () => {
+    testIconLoader.setShouldIconsLoad(true);
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kSensitiveContentBlock,
+    });
+    await microtasksFinished();
+    assertEquals('cr:error', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'red',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          isInsecure: true,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      isInsecure: true,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousFile,
-          safeBrowsingState: SafeBrowsingState.kNoSafeBrowsing,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousFile,
+      safeBrowsingState: SafeBrowsingState.kNoSafeBrowsing,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousFile,
-          safeBrowsingState: SafeBrowsingState.kEnhancedProtection,
-          hasSafeBrowsingVerdict: true,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousFile,
+      safeBrowsingState: SafeBrowsingState.kEnhancedProtection,
+      hasSafeBrowsingVerdict: true,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/test/data/webui/downloads/item_test.ts b/chrome/test/data/webui/downloads/item_test.ts
index 8ebf725..d34f614 100644
--- a/chrome/test/data/webui/downloads/item_test.ts
+++ b/chrome/test/data/webui/downloads/item_test.ts
@@ -111,227 +111,209 @@
     assertTrue(item.getFileIcon().hidden);
   });
 
-  test(
-      'icon overridden by display type', async () => {
-        testIconLoader.setShouldIconsLoad(true);
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kSensitiveContentBlock,
-        });
-        await microtasksFinished();
-        assertEquals(
-            'cr:error', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'red',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+  test('icon overridden by display type', async () => {
+    testIconLoader.setShouldIconsLoad(true);
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kSensitiveContentBlock,
+    });
+    await microtasksFinished();
+    assertEquals('cr:error', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'red',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          isInsecure: true,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      isInsecure: true,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousFile,
-          safeBrowsingState: SafeBrowsingState.kNoSafeBrowsing,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousFile,
+      safeBrowsingState: SafeBrowsingState.kNoSafeBrowsing,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousFile,
-          safeBrowsingState: SafeBrowsingState.kEnhancedProtection,
-          hasSafeBrowsingVerdict: true,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousFile,
+      safeBrowsingState: SafeBrowsingState.kEnhancedProtection,
+      hasSafeBrowsingVerdict: true,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousFile,
-          safeBrowsingState: SafeBrowsingState.kStandardProtection,
-          hasSafeBrowsingVerdict: false,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousFile,
+      safeBrowsingState: SafeBrowsingState.kStandardProtection,
+      hasSafeBrowsingVerdict: false,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDeepScannedFailed,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDeepScannedFailed,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals('cr:warning', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousUrl,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousUrl,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'downloads:dangerous',
-            item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'red',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
+    assertEquals(
+        'downloads:dangerous', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'red',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kCookieTheft,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kCookieTheft,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'downloads:dangerous',
-            item.shadowRoot.querySelector('cr-icon')!.icon);
-        assertTrue(item.$['file-icon'].hidden);
-        assertEquals(
-            'red',
-            item.shadowRoot.querySelector('cr-icon')!.getAttribute(
-                'icon-color'));
-      });
+    assertEquals(
+        'downloads:dangerous', item.shadowRoot.querySelector('cr-icon')!.icon);
+    assertTrue(item.$['file-icon'].hidden);
+    assertEquals(
+        'red',
+        item.shadowRoot.querySelector('cr-icon')!.getAttribute('icon-color'));
+  });
 
-  test(
-      'description color set by display type', async () => {
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kSensitiveContentBlock,
-        });
-        await microtasksFinished();
+  test('description color set by display type', async () => {
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kSensitiveContentBlock,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'red',
-            item.shadowRoot.querySelector('.description')!.getAttribute(
-                'description-color'));
+    assertEquals(
+        'red',
+        item.shadowRoot.querySelector('.description')!.getAttribute(
+            'description-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          isInsecure: true,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      isInsecure: true,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'grey',
-            item.shadowRoot.querySelector('.description')!.getAttribute(
-                'description-color'));
+    assertEquals(
+        'grey',
+        item.shadowRoot.querySelector('.description')!.getAttribute(
+            'description-color'));
 
-        item.data = createDownload({
-          filePath: 'unique1',
-          hideDate: false,
-          dangerType: DangerType.kDangerousFile,
-          safeBrowsingState: SafeBrowsingState.kNoSafeBrowsing,
-        });
-        await microtasksFinished();
+    item.data = createDownload({
+      filePath: 'unique1',
+      hideDate: false,
+      dangerType: DangerType.kDangerousFile,
+      safeBrowsingState: SafeBrowsingState.kNoSafeBrowsing,
+    });
+    await microtasksFinished();
 
-        assertEquals(
-            'grey',
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Unblocked :// Characters on `data:text`origin value lead to File Source Origin Spoofing

Steps to reproduce the problem

  1. Access poc.html
  2. Press download
  3. Go to chrome://downloads and you will see the origin will got blank
  4. When you press the link button, you will see that https://google.com will be set as an origin

Problem Description

This vulnerability occurs when the :// characters is not blocked. As a result, an attacker can spoof the file’s source origin, making it appear as if the file is being downloaded from a trusted source. Chromium should implement a similar prevention mechanism as used in filename protection, where :// characters got removed or blocked, i’ve attach the video how :// characters got blocked when inserted inside of filename.

Summary

Unblocked :// Characters on data:textorigin value lead to File Source Origin Spoofing

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A

View on issue tracker
Links in the report