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 <[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
- 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