High watch CVSS 8.8 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
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.
Tracker486761172
Fix commit9c229906472a (chromium/src) +71/-15
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedrachmat.abdul.ro
Reported2026-02-23 (per vendor advisory)
Fix landed2026-03-18 (public git)
Disclosed2026-05-12
Patch gap55 days — fix public in source before the advisory

Background

`GenerateSafeFileName`
Chrome’s download filename sanitizer that decides a download’s final extension and whether the file type is dangerous.
`RemoveEnvVarFromFileName`
A ui helper that strips Windows environment-variable syntax (e.g. %...%) from a filename so it is not later evaluated by the shell/dialog.
Windows "Save As" basename trimming
The native Windows file dialog silently strips trailing dots and spaces from a filename’s basename before saving, which can change the effective extension.
Internet Shortcut (`.url`)
A Windows file type that can execute/navigate when opened, so hiding a .url extension behind a benign-looking one is dangerous.

Root Cause Analysis

The pre-patch code performed a single pass of ui::RemoveEnvVarFromFileName and then a trailing-dot/whitespace trim loop, but these steps interacted with the native Windows dialog trimming in a way the sanitizer did not model. An attacker-controlled filename such as photo.jpg %%.url had its %% removed by RemoveEnvVarFromFileName, leaving photo.jpg .url; the sanitizer saw .url preceded by a space so its extension analysis was defeated, and separately the Windows “Save As” dialog natively strips the trailing space of the basename, so the file was actually written as a .url Internet Shortcut while being displayed to the user as photo.jpg. The invariant violated is that the extension Chrome evaluates for danger must equal the extension Windows ultimately saves.

The fix replaces the one-shot logic with SanitizeDownloadFileName, an iterative loop that alternately re-runs env-var removal and reproduces the dialog’s basename dot/space trimming (splitting off extension, trimming the basename, reassembling, then trimming the whole name) until the name stabilizes, so any hidden extension is surfaced and handed to GenerateSafeFileName.

Key insight
Sanitization was single-pass and ignored that Windows re-trims the basename after env-var stripping, leaving a concealed executable extension; the fix iterates the strip-and-trim to a fixed point so the extension Chrome checks matches the one Windows will actually save.

Attack Path

  1. Craft a deceptive filename The attacker serves a download whose suggested name embeds env-var characters and a trailing space before a dangerous extension, e.g. photo.jpg%20%25%25.url (decoded photo.jpg %%.url), per added test case 7.
  2. Single-pass strip leaves residue RemoveEnvVarFromFileName removes %% once, yielding photo.jpg .url, which the old loop does not re-process into a correct extension view.
  3. Dialog hides the real extension In the “Save As” dialog the native basename trim drops the trailing space so the user sees photo.jpg, concealing the true .url extension that GenerateSafeFileName was not made aware of.
  4. Dangerous file saved unflagged The file is written as an Internet Shortcut without the sanitization/danger evaluation that a .url would normally receive, so the user is deceived into saving an executable-class file.

Impact Assessment

Per the vendor advisory this is a high-severity insufficient-input-validation issue in Downloads: a malicious site can disguise a dangerous Windows file type (such as a .url Internet Shortcut) as a benign file in the Save As dialog, defeating Chrome’s extension-based danger checks. Preconditions are a Windows client and a user accepting the download; the advisory does not report in-the-wild exploitation, and any resulting code execution depends on the user later opening the saved shortcut (inferred).

Changed Functions

FunctionChangeNotes
while
chrome/browser/download/download_target_determiner.cc
modified

Files Changed

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

Audit Directions

  • Sanitizer vs. platform post-processing mismatch
    Look for filename/path sanitizers that run once but feed into OS APIs (dialogs, shells) that further mutate the string, where the final form is never re-validated.
  • Order-dependent transforms on untrusted names
    Audit chained transforms (unescape, env-var strip, trim) on attacker-controlled filenames where a later step can resurrect a dangerous extension hidden by an earlier one; prefer fixed-point iteration.
  • Extension-based danger checks
    Review every place a decision about download danger is keyed on the parsed extension, ensuring the parsed value equals what will actually be written to disk on each platform.
From 9c229906472adc5a601eef5e419b2a6726e91cc7 Mon Sep 17 00:00:00 2001
From: Andrew Liu <liu@chromium.org>
Date: Wed, 18 Mar 2026 11:36:07 -0700
Subject: [PATCH] Fix Windows "Save As" dialog extension bypass

When a user downloads a file, Chrome sanitizes the filename by
removing environment variables to prevent evaluation on the client.
However, an attacker could supply a filename like "photo.jpg %%.url".
The single-pass environment variable removal would strip "%%" and
leave "photo.jpg .url".

Because the Windows "Save As"" dialog natively strips trailing spaces
and dots from the basename of a file, "photo.jpg .url" is displayed
to the user simply as "photo.jpg", hiding the true ".url" extension.
Because Chrome's GenerateSafeFileName was unaware of this native
Windows trimming, it allowed the file to be saved as an Internet
Shortcut without proper sanitization.

This CL fixes the bypass by implementing an iterative sanitization
loop in `DownloadTargetDeterminer`. This loop mimics the exact basename
trimming behavior of the Windows "Save As" dialog, reconstructing and
cleaning the filename until it stabilizes. This ensures that any
hidden extensions are exposed and properly evaluated by
`GenerateSafeFileName`.

Bug: 486761172
Change-Id: I532f733d8eee0ee23f34eebd7d44f6c66a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7639229
Reviewed-by: Min Qin <qinmin@chromium.org>
Reviewed-by: Lily Chen <chlily@chromium.org>
Commit-Queue: Andrew Liu <liu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1601428}
---

diff --git a/chrome/browser/download/download_target_determiner.cc b/chrome/browser/download/download_target_determiner.cc
index 1dd2dbab..630f43c 100644
--- a/chrome/browser/download/download_target_determiner.cc
+++ b/chrome/browser/download/download_target_determiner.cc
@@ -13,6 +13,7 @@
 #include "base/location.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/rand_util.h"
+#include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/single_thread_task_runner.h"
@@ -120,6 +121,52 @@
   }
 }
 
+#if BUILDFLAG(IS_WIN)
+// Iteratively sanitizes a download filename for Windows by removing environment
+// variables and trimming trailing dots and spaces. This mimics the behavior of
+// the Windows "Save As" dialog to ensure extension checks are accurate.
+// See crbug.com/41486690 and crbug.com/486079015 for more context.
+std::wstring SanitizeDownloadFileName(std::wstring_view initial_name) {
+  std::wstring current_name(initial_name);
+
+  const auto trim_trailing_dots_and_whitespace = [](std::wstring_view s) {
+    while (!s.empty() &&
+           (s.back() == L'.' || base::IsUnicodeWhitespace(s.back()))) {
+      s.remove_suffix(1);
+    }
+    return s;
+  };
+
+  while (true) {
+    std::wstring next_name =
+        ui::RemoveEnvVarFromFileName<wchar_t>(current_name, L"%");
+
+    const base::FilePath next_path(next_name);
+    const std::wstring extension = next_path.Extension();
+
+    std::wstring_view current_basename = next_path.value();
+    CHECK_LE(extension.length(), current_basename.length());
+    current_basename.remove_suffix(extension.length());
+
+    // Iteratively trim trailing dots and spaces from the basename since
+    // the Windows Save As dialog natively strips trailing spaces and dots
+    // from the basename before saving. We must simulate this to check the
+    // true final extension.
+    current_basename = trim_trailing_dots_and_whitespace(current_basename);
+
+    next_name = std::wstring(current_basename) + extension;
+
+    // Then, trim trailing dots and whitespace from the entire filename again.
+    next_name.resize(trim_trailing_dots_and_whitespace(next_name).length());
+
+    if (next_name.length() == current_name.length()) {
+      return current_name;
+    }
+    current_name = std::move(next_name);
+  }
+}
+#endif  // BUILDFLAG(IS_WIN)
+
 }  // namespace
 
 DownloadTargetDeterminerDelegate::~DownloadTargetDeterminerDelegate() = default;
@@ -597,21 +644,9 @@
       // Windows prompt dialog will resolve all env variables in the file name,
       // which may generate unexpected results. Remove env variables from the
       // file name first.
-      std::wstring sanitized_name = ui::RemoveEnvVarFromFileName<wchar_t>(
-          virtual_path_.BaseName().value(), L"%");
-      // 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;
-      }
+      std::wstring sanitized_name =
+          SanitizeDownloadFileName(virtual_path_.BaseName().value());
+
       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 890c8305..c875ed04 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -2721,6 +2721,27 @@
        FILE_PATH_LITERAL("foo.download"),
        DownloadItem::TARGET_DISPOSITION_PROMPT,
 
+       EXPECT_CRDOWNLOAD},
+      {// 7: Prevent hiding extensions (like .url) in the Windows Save As dialog
+       // by wrapping trailing spaces inside environment variables.
+       SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+       DownloadFileType::NOT_DANGEROUS,
+       "http://example.com/photo.jpg%20%25%25.url", "text/plain",
+       FILE_PATH_LITERAL(""),
+
+       FILE_PATH_LITERAL("photo.jpg.url"),
+       DownloadItem::TARGET_DISPOSITION_PROMPT,
+
+       EXPECT_CRDOWNLOAD},
+
+      {// 8: Ensure completely empty filenames made of just dots and environment
+       // variables collapse safely without retaining any dangerous extensions.
+       SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+       DownloadFileType::NOT_DANGEROUS, "http://example.com/.%25%25.%25%25",
+       "text/plain", FILE_PATH_LITERAL(""),
+
+       FILE_PATH_LITERAL("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 890c8305..c875ed04 100644
--- a/chrome/browser/download/download_target_determiner_unittest.cc
+++ b/chrome/browser/download/download_target_determiner_unittest.cc
@@ -2721,6 +2721,27 @@
        FILE_PATH_LITERAL("foo.download"),
        DownloadItem::TARGET_DISPOSITION_PROMPT,
 
+       EXPECT_CRDOWNLOAD},
+      {// 7: Prevent hiding extensions (like .url) in the Windows Save As dialog
+       // by wrapping trailing spaces inside environment variables.
+       SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+       DownloadFileType::NOT_DANGEROUS,
+       "http://example.com/photo.jpg%20%25%25.url", "text/plain",
+       FILE_PATH_LITERAL(""),
+
+       FILE_PATH_LITERAL("photo.jpg.url"),
+       DownloadItem::TARGET_DISPOSITION_PROMPT,
+
+       EXPECT_CRDOWNLOAD},
+
+      {// 8: Ensure completely empty filenames made of just dots and environment
+       // variables collapse safely without retaining any dangerous extensions.
+       SAVE_AS, download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
+       DownloadFileType::NOT_DANGEROUS, "http://example.com/.%25%25.%25%25",
+       "text/plain", FILE_PATH_LITERAL(""),
+
+       FILE_PATH_LITERAL("download"), DownloadItem::TARGET_DISPOSITION_PROMPT,
+
        EXPECT_CRDOWNLOAD}};
 
   RunTestCasesWithActiveItem(kSaveEnvPathTestCases);
Loading diff…

Original Bug Report

reported by ra...@gmail.com

Chrome on Windows can be tricked into saving a dangerous .url InternetShortcut without the .download

Steps to reproduce the problem

  1. Run: poc_alt.py (serves on 127.0.0.1:8001).
  2. Chrome on Windows → open http://127.0.0.1:8001/.
  3. Right-click “Bypass: photo.jpg %25%25.url” → “Save link as…”.
  4. Save dialog suggests photo.jpg .url (type: Internet Shortcut). No .download rewrite.
  5. Save & open: launches calc.exe (payload).

Problem Description

  • Windows shell strips %…% from the suggested filename, making the basename appear extensionless.
  • GenerateSafeFileName() in download_target_determiner.cc (Windows path) calls net::GenerateSafeFileName() with old_extension captured before sanitization.
  • Because the basename now seems extensionless, the dangerous-extension rewrite is skipped, and .url is reattached verbatim.

Summary

Chrome on Windows can be tricked into saving a dangerous .url InternetShortcut without the .download

Additional Data

Category: Security
Chrome Channel: Stable
Regression: N/A \

View on issue tracker
Links in the report