Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Downloads
DescriptionInsufficient validation of untrusted input in Downloads
ComponentDownloads
Bug ClassLogic Error
Tracker444803530
Fix commit5e9cd600b865 (chromium/src) +25/-2
CISA KEVNot listed
CreditedAzur
Disclosed2026-01-13

Files Changed

  • chrome/browser/download/download_target_determiner.cc
  • chrome/browser/download/download_target_determiner_unittest.cc
From 5e9cd600b865a126eb6d14ffb06dd311d6267faa Mon Sep 17 00:00:00 2001
From: Kovacs Zeteny <brightbulbapp@gmail.com>
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 <qinmin@chromium.org>
Reviewed-by: Xinghui Lu <xinghuilu@chromium.org>
Commit-Queue: Min Qin <qinmin@chromium.org>
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(""),
Loading diff…

Regression Test / PoC

shipped with the fix
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(""),
Loading diff…

Original Bug Report

reported by br...@gmail.com

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

  1. Run the attached PoC server (poc.py). It serves a page with two tiles and download endpoints that set Content-Disposition.
  2. Visit http://127.0.0.1:8000/ in Chrome on Windows.
  3. Right-click → “Save link as…” on:
    • Control: /download?case=lnk_plainfilename="file.lnk" (should be rewritten to .download).
    • Bypass: /download?case=lnk_pctfilename="file.lnk %%" (often shows file.lnk .lnk).
  4. 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:

View on issue tracker
Links in the report