Medium watch CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Downloads
DescriptionInsufficient validation of untrusted input in Downloads
ComponentDownloads
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker486079015
Fix commit8c1145d4b23d (chromium/src) +21/-6
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
Crediteddaffainfo
Reported2026-02-20 (per vendor advisory)
Fix landed2026-03-04 (public git)
Disclosed2026-04-07
Patch gap34 days — fix public in source before the advisory

Background

Download target determiner
the browser-process component that decides the final on-disk filename for a download, including sanitizing an attacker-influenced suggested name.
`RemoveEnvVarFromFileName`
a Windows helper that strips environment-variable-style tokens (delimited by %) from a filename so they cannot be expanded.
Trailing-dot/whitespace stripping
Windows silently drops trailing . and spaces when opening a path, so Chrome must strip them first to see the file’s true final extension before applying dangerous-file policy.
Stale extension
a dangerous extension (e.g. .lnk) left intact because normalization failed to remove trailing junk that was masking it.

Root Cause Analysis

The Windows filename-sanitization block in download_target_determiner.cc normalized a suggested name by, in a single pass, popping trailing . characters in a while loop and then calling base::TrimWhitespace(..., TRIM_TRAILING) exactly once. This one-shot ordering fails when dots and whitespace are interleaved at the end of the name (as produced after RemoveEnvVarFromFileName removes multiple %% tokens): stripping dots can expose trailing whitespace, and trimming whitespace can expose further trailing dots, but the second kind of junk is never revisited. For input like foo.lnk .%% .%%, the old code left a residual trailing ./space sequence so the dangerous .lnk extension survived normalization.

The fix wraps both steps in an outer while loop that records the length, strips trailing dots, trims trailing whitespace, and repeats until the length stops changing, guaranteeing all interleaved trailing dots and whitespace are removed so the real extension (here, none — yielding the default foo.download) is what policy evaluates.

Key insight
The core mistake was normalizing trailing dots and whitespace in a single non-repeating pass, which leaves interleaved junk (and the dangerous extension it hides) intact; the fix loops the strip-dots/trim-whitespace sequence until the name stabilizes.

Attack Path

  1. Serve a crafted download name A malicious site offers a download whose suggested filename ends with interleaved dots, spaces, and double-percent tokens, e.g. foo.lnk .%% .%%.
  2. Partial normalization RemoveEnvVarFromFileName strips the %-delimited tokens, and the old single-pass dot/whitespace trimming leaves a residual trailing ./space sequence.
  3. Stale extension survives Because normalization stops early, the dangerous .lnk extension remains on the name instead of being stripped to the safe default.
  4. Policy bypass The download is saved/classified against the un-normalized name, so a dangerous file type can masquerade past the dangerous-extension handling (inferred; the diff shows the normalization gap, not the downstream classification outcome).

Impact Assessment

A malicious web page can influence a download’s filename so Chrome’s Windows normalization leaves a dangerous extension (e.g. .lnk) that it was supposed to strip, potentially evading dangerous-file handling in the browser process; exploitation requires the user to initiate/accept the download (the test uses the SAVE_AS path) and is Windows-specific (wchar_t, RemoveEnvVarFromFileName<wchar_t>). The vendor advisory rates this medium (insufficient validation of untrusted input in Downloads) and does not report in-the-wild exploitation; the concrete security bypass is (inferred) from the stale-extension behavior.

Files Changed

  • chrome/browser/download/download_target_determiner.cc
  • chrome/browser/download/download_target_determiner_unittest.cc

Audit Directions

  • Single-pass sanitization of adversarial strings
    look for filename/path normalizers that strip two or more trailing character classes once each; interleaved input defeats non-iterated passes.
  • Order-dependent trimming
    flag any “remove A then remove B” normalization where removing B can re-expose A (or vice versa) and no fixed-point loop guarantees convergence.
  • Post-token-removal residue
    after helpers like RemoveEnvVarFromFileName delete substrings, re-audit downstream trimming for the new trailing junk those deletions can expose, especially on Windows where trailing dots/spaces are semantically dropped.
From 8c1145d4b23d6a8cf2867b922716153f6ea72d9e Mon Sep 17 00:00:00 2001
From: Andrew Liu <[email protected]>
Date: Wed, 04 Mar 2026 12:47:47 -0800
Subject: [PATCH] Filter out multiple "double percent signs" when normalizing download files

Bug: 486079015
Change-Id: I7047e2f0a210de1ae17cbb95884f3a6c6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7629495
Reviewed-by: Min Qin <[email protected]>
Reviewed-by: Lily Chen <[email protected]>
Commit-Queue: Andrew Liu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1594178}
---

diff --git a/chrome/browser/download/download_target_determiner.cc b/chrome/browser/download/download_target_determiner.cc
index 760025c..1dd2dbab 100644
--- a/chrome/browser/download/download_target_determiner.cc
+++ b/chrome/browser/download/download_target_determiner.cc
@@ -599,13 +599,19 @@
       // file name first.
       std::wstring sanitized_name = ui::RemoveEnvVarFromFileName<wchar_t>(
           virtual_path_.BaseName().value(), L"%");
-      // remove leading "." to avoid resorting to potential extension
-      // bug: 41486690
-      while (!sanitized_name.empty() && sanitized_name.back() == L'.') {
-          sanitized_name.pop_back();
+      // Remove trailing "." and whitespace to avoid resorting to potential
+      // extensions.
+      // See crbug.com/41486690 and crbug.com/486079015 for more context.
+      while (!sanitized_name.empty()) {
+        size_t length = sanitized_name.length();
+        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 (length == sanitized_name.length())
+          break;
       }
-      // 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));
diff --git a/chrome/browser/download/download_target_determiner_unittest.cc b/chrome/browser/download/download_target_determiner_unittest.cc
index dff43fc..890c8305 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -2712,6 +2712,15 @@
 
        FILE_PATH_LITERAL("download"), DownloadItem::TARGET_DISPOSITION_PROMPT,
 
+       EXPECT_CRDOWNLOAD},
+      {// 6: Multiple env vars need to be filtered out.
+       SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+       DownloadFileType::NOT_DANGEROUS, "http://example.com/foo.lnk .%% .%%",
+       "application/octet-stream", FILE_PATH_LITERAL(""),
+
+       FILE_PATH_LITERAL("foo.download"),
+       DownloadItem::TARGET_DISPOSITION_PROMPT,
+
        EXPECT_CRDOWNLOAD}};
 
   RunTestCasesWithActiveItem(kSaveEnvPathTestCases);
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 dff43fc..890c8305 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -2712,6 +2712,15 @@
 
        FILE_PATH_LITERAL("download"), DownloadItem::TARGET_DISPOSITION_PROMPT,
 
+       EXPECT_CRDOWNLOAD},
+      {// 6: Multiple env vars need to be filtered out.
+       SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+       DownloadFileType::NOT_DANGEROUS, "http://example.com/foo.lnk .%% .%%",
+       "application/octet-stream", FILE_PATH_LITERAL(""),
+
+       FILE_PATH_LITERAL("foo.download"),
+       DownloadItem::TARGET_DISPOSITION_PROMPT,
+
        EXPECT_CRDOWNLOAD}};
 
   RunTestCasesWithActiveItem(kSaveEnvPathTestCases);
Loading diff…

Original Bug Report

reported by [email protected]

.lnk File Download Bypass Using "Save As" Link (Bypass of #444803530)

Steps to reproduce the problem

  1. Run poc.py, and then open http://127.0.0.1:8000/
  2. Right click download me hyperlink and then choose Save File As
  3. Choose Save button and .lnk file will be downloaded

Problem Description

This is a bypass of https://issues.chromium.org/issues/444803530, where an attacker uses a double percent sign (%%) to bypass Chrome’s dangerous file protection. Although it has already been fixed, it can still be bypassed by using a double “double percent sign,” such as: file.lnk .%% .%%

Summary

.lnk File Download Bypass Using “Save As” Link (Bypass of #444803530)

Additional Data

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

View on issue tracker
Links in the report