CVE-2026-0903
Overview
Files Changed
chrome/browser/download/download_target_determiner.ccchrome/browser/download/download_target_determiner_unittest.cc
Patch
From 5e9cd600b865a126eb6d14ffb06dd311d6267faa Mon Sep 17 00:00:00 2001 From: Kovacs Zeteny <[email protected]> Date: Fri, 31 Oct 2025 10:07:46 -0700 Subject: [PATCH] Windows: fix bug that could re-attach dangerous extension This CL ensures that after environment variable markers and trailing characters are stripped from a filename on Windows, the extension is derived from the sanitized basename. Without this, Chrome could fall back to re-attaching the original extension, leading to cases like ".txt .txt" or allowing blocked types such as ".lnk" to reappear. Bug: 444803530 Change-Id: Ie8f19b7ef4777721e7ac1376957cda55250142b4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6990751 Reviewed-by: Min Qin <[email protected]> Reviewed-by: Xinghui Lu <[email protected]> Commit-Queue: Min Qin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1538683} --- diff --git a/chrome/browser/download/download_target_determiner.cc b/chrome/browser/download/download_target_determiner.cc index 1d88eb7..9e019f4 100644 --- a/chrome/browser/download/download_target_determiner.cc +++ b/chrome/browser/download/download_target_determiner.cc @@ -606,13 +606,17 @@ while (!sanitized_name.empty() && sanitized_name.back() == L'.') { sanitized_name.pop_back(); } + // trim trailing whitespace (space, tab, NBSP) to prevent stale extensions + base::TrimWhitespace(sanitized_name, base::TrimPositions::TRIM_TRAILING, &sanitized_name); if (sanitized_name.empty()) { sanitized_name = base::UTF8ToWide( l10n_util::GetStringUTF8(IDS_DEFAULT_DOWNLOAD_FILENAME)); } sanitized_path = virtual_path_.DirName().Append(base::FilePath(sanitized_name)); - GenerateSafeFileName(&sanitized_path, virtual_path_.Extension(), + const base::FilePath::StringType post_sanitize_ext = + base::FilePath(sanitized_name).Extension(); + GenerateSafeFileName(&sanitized_path, post_sanitize_ext, download_->GetMimeType()); #endif // BUILDFLAG(IS_WIN) delegate_->RequestConfirmation( diff --git a/chrome/browser/download/download_target_determiner_unittest.cc b/chrome/browser/download/download_target_determiner_unittest.cc index f3043a21..2505800 100644 --- a/chrome/browser/download/download_target_determiner_unittest.cc +++ b/chrome/browser/download/download_target_determiner_unittest.cc @@ -2714,7 +2714,26 @@ DownloadItem::TARGET_DISPOSITION_PROMPT, EXPECT_CRDOWNLOAD}, - {// 3: File name is an env var. + {// 3: Double extension bug leading to dangerous extensions after removing env var. + SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS, + DownloadFileType::NOT_DANGEROUS, "http://example.com/foo2.lnk %%", + "application/octet-stream", FILE_PATH_LITERAL(""), + + FILE_PATH_LITERAL("foo2.download"), + DownloadItem::TARGET_DISPOSITION_PROMPT, + + EXPECT_CRDOWNLOAD}, + {// 4: Unicode char bug leading to dangerous extensions after removing env var. + // NOTE: The space before "%%" is a non-breaking space (U+00A0), not a normal space. + SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS, + DownloadFileType::NOT_DANGEROUS, "http://example.com/foo2.lnk %%", + "application/octet-stream", FILE_PATH_LITERAL(""), + + FILE_PATH_LITERAL("foo2.download"), + DownloadItem::TARGET_DISPOSITION_PROMPT, + + EXPECT_CRDOWNLOAD}, + {// 5: File name is an env var. SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS, DownloadFileType::NOT_DANGEROUS, "http://example.com/%foo.txt%", "text/plain", FILE_PATH_LITERAL(""),
Regression Test / PoC
diff --git a/chrome/browser/download/download_target_determiner_unittest.cc b/chrome/browser/download/download_target_determiner_unittest.cc
index f3043a21..2505800 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -2714,7 +2714,26 @@
DownloadItem::TARGET_DISPOSITION_PROMPT,
EXPECT_CRDOWNLOAD},
- {// 3: File name is an env var.
+ {// 3: Double extension bug leading to dangerous extensions after removing env var.
+ SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+ DownloadFileType::NOT_DANGEROUS, "http://example.com/foo2.lnk %%",
+ "application/octet-stream", FILE_PATH_LITERAL(""),
+
+ FILE_PATH_LITERAL("foo2.download"),
+ DownloadItem::TARGET_DISPOSITION_PROMPT,
+
+ EXPECT_CRDOWNLOAD},
+ {// 4: Unicode char bug leading to dangerous extensions after removing env var.
+ // NOTE: The space before "%%" is a non-breaking space (U+00A0), not a normal space.
+ SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+ DownloadFileType::NOT_DANGEROUS, "http://example.com/foo2.lnk %%",
+ "application/octet-stream", FILE_PATH_LITERAL(""),
+
+ FILE_PATH_LITERAL("foo2.download"),
+ DownloadItem::TARGET_DISPOSITION_PROMPT,
+
+ EXPECT_CRDOWNLOAD},
+ {// 5: File name is an env var.
SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
DownloadFileType::NOT_DANGEROUS, "http://example.com/%foo.txt%",
"text/plain", FILE_PATH_LITERAL(""),
Original Bug Report
Windows download logic flaw: % triggers double extension sanitization bypass (.lnk .lnk, .scf .scf)
Description
Chrome on Windows can be tricked into delivering dangerous file types (for example, .lnk or .scf) despite the download sanitization that should rewrite them to .download.
When a suggested filename includes certain % sequences after a space following the extension, Chrome strips the % tokens, then re-applies the original extension, yielding a double extension (for example, file.lnk .lnk). As a result, the forbidden extension survives instead of being rewritten.
This appears to be a Windows-only issue, because the Windows file dialog expands or strips %…% patterns and the sanitization path subsequently reattaches the stale extension.
Examples
Content-Disposition: attachment; filename="file.lnk %%"
Result:file.lnk .lnk(expected: file.download)Content-Disposition: attachment; filename="file.scf .%%"
Result:file.scf .scf(expected: file.download)
Expected behavior Dangerous extensions (for example, .lnk, .scf) should always be rewritten to .download. No combination of spaces or % should allow the original extension to persist or reappear.
Observed behavior
Appending " %%" (or similar) after the extension leads to a double extension, allowing .lnk or .scf to survive (for example, file.lnk .lnk). This bypasses Chrome’s dangerous-file protection and presents users with a misleading, executable shortcut.
Security impact This is a sanitization bypass that enables delivery of files Chrome intends to block. A user can be tricked into saving and executing a Windows shortcut (.lnk) that appears benign (for example, disguised as an “image” or document link). Impact includes code execution or persistence via shortcut abuse.
Steps to reproduce
- Run the attached PoC server (poc.py). It serves a page with two tiles and download endpoints that set Content-Disposition.
- Visit
http://127.0.0.1:8000/in Chrome on Windows. - Right-click → “Save link as…” on:
- Control:
/download?case=lnk_plain→filename="file.lnk"(should be rewritten to .download). - Bypass:
/download?case=lnk_pct→filename="file.lnk %%"(often shows file.lnk .lnk).
- Control:
- Observe the suggested filename in the Save dialog and the final saved name.
Note: The PoC serves a harmless payload.download file so the saved file is never HTML. It isolates the filename/extension behavior under Content-Disposition.
Affected versions (observed)
- Stable: 140.0.7339.128
- Beta: 141.0.7390.16
- Dev: 142.0.7405.4
Bisect results (observed)
- Last known developer build without the issue:
Win/1023679(105.0.5178.0 Dev). - First developer build with the issue:
Win/1025642(105.0.5190.0 Dev). - This aligns with commit 5e105f0bc3443e873924b92778574ffbb792c645, which landed in 105.0.5183.0 and modified the Windows filename sanitization logic.
Root cause (high level)
- On Windows, the Save dialog layer strips
%…%from the basename. With inputs like"file.lnk %%", this leaves"file.lnk "(with a trailing space). - Later, the download target logic calls safe-name handling with the pre-sanitization extension (for example, .lnk) while the post-sanitized basename appears extensionless.
- The safe-name logic then re-attaches the old extension, producing file.lnk .lnk and bypassing the .lnk → .download rewrite.
PoC
Attach poc.py (serves a self-contained HTML page and Content-Disposition endpoints). Usage: