Chrome · Downloads
CVE-2026-5897
Logic Error in Downloads
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/resources/downloads/item.ts |
modified |
Files Changed
chrome/app/generated_resources.grdchrome/app/generated_resources_grd/IDS_DOWNLOADS_TOAST_COPIED_LINK.png.sha1chrome/browser/resources/downloads/item.tschrome/browser/ui/webui/downloads/downloads_ui.ccchrome/test/data/webui/downloads/item_test.ts
Patch
From 97ae0a483361b3253153191df0653e928ad0d8ec Mon Sep 17 00:00:00 2001
From: Andrew Liu <liu@chromium.org>
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 <chlily@chromium.org>
Commit-Queue: Andrew Liu <liu@chromium.org>
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 fr...@gmail.com
Unblocked :// Characters on `data:text`origin value lead to File Source Origin Spoofing
Steps to reproduce the problem
- Access poc.html
- Press download
- Go to chrome://downloads and you will see the origin will got blank
- 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
References
On This Page