Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in PlatformIntegration
DescriptionInappropriate implementation in PlatformIntegration
ComponentPlatformIntegration
Bug ClassLogic Error
Tracker476862276
Fix commit4333ac30e1d4 (chromium/src) +29/-3
CISA KEVNot listed
CreditedHan Liu (Xi’an Jiaotong University, School of Cyber Science and Engineering)
Disclosed2026-06-02

Files Changed

  • ui/base/win/shell.cc
From 4333ac30e1d439c5f1781c3c57472d397e4ba6f3 Mon Sep 17 00:00:00 2001
From: Robert Liao <[email protected]>
Date: Mon, 26 Jan 2026 10:36:28 -0800
Subject: [PATCH] Manually Parse Paths to IDLists for ShellExecute

ShellExecute may perform incorrect legacy resolution for paths
that do not appear to have a valid extension (Example: file. ext).

Extensions cannot have spaces like in "file. ext", which convinces
ShellExecute to think that this is a legacy path like "chrome" that
needs to resolve to "chrome.exe".

ShellExecute will append one of the following extensions in an attempt
to "find" the corrected path, even if the provided path already
exists:
  - .pif
  - .com
  - .exe
  - .bat
  - .lnk
  - .cmd

There doesn't appear to be a way to tell ShellExecute through
SHELLEXECUTEINFO to avoid performing this legacy resolution other than
having the caller pre-parse the path into an ID list and passing the
ID list through SHELLEXECUTEINFO::lpIDList.

Fixed: 476862276
Bug: 478206473
Change-Id: I6c9b1d7b66a61fc4ab84ab7d5296ab099d96aa2b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7514332
Commit-Queue: Jesse McKenna <[email protected]>
Auto-Submit: Robert Liao <[email protected]>
Reviewed-by: Jesse McKenna <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1574631}
---

diff --git a/ui/base/win/shell.cc b/ui/base/win/shell.cc
index c2a8964..d076c9a 100644
--- a/ui/base/win/shell.cc
+++ b/ui/base/win/shell.cc
@@ -4,14 +4,17 @@
 
 #include "ui/base/win/shell.h"
 
-#include <dwmapi.h>
+// clang-format off
 #include <shlobj.h>  // Must be before propkey.
+// clang-format on
 
+#include <dwmapi.h>
 #include <propkey.h>
 #include <shellapi.h>
 #include <wrl/client.h>
 
 #include "base/debug/alias.h"
+#include "base/feature_list.h"
 #include "base/files/file.h"
 #include "base/files/file_path.h"
 #include "base/native_library.h"
@@ -20,6 +23,7 @@
 #include "base/strings/string_util.h"
 #include "base/threading/scoped_blocking_call.h"
 #include "base/threading/scoped_thread_priority.h"
+#include "base/win/atl.h"
 #include "base/win/win_util.h"
 #include "ui/base/ui_base_switches.h"
 
@@ -27,6 +31,9 @@
 
 namespace {
 
+BASE_FEATURE(kManuallyParsePathForShellExecute,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 // Default ShellExecuteEx flags used with "openas", "explore", and default
 // verbs.
 //
@@ -46,7 +53,9 @@
   base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                 base::BlockingType::WILL_BLOCK);
 
-  SHELLEXECUTEINFO sei = {sizeof(sei)};
+  SHELLEXECUTEINFO sei = {};
+  sei.cbSize = sizeof(sei);
+
   if (!class_name.empty()) {
     sei.lpClass = class_name.c_str();
     mask = (mask | SEE_MASK_CLASSNAME);
@@ -55,11 +64,28 @@
   sei.fMask = mask;
   sei.nShow = SW_SHOWNORMAL;
   sei.lpVerb = (verb.empty() ? nullptr : verb.c_str());
-  sei.lpFile = path.c_str();
   sei.lpDirectory =
       (working_directory.empty() ? nullptr : working_directory.c_str());
   sei.lpParameters = (args.empty() ? nullptr : args.c_str());
 
+  CComHeapPtr<ITEMIDLIST_ABSOLUTE> path_id_list;
+  if (base::FeatureList::IsEnabled(kManuallyParsePathForShellExecute)) {
+    // ShellExecute will perform legacy resolution of a path if it can't detect
+    // an extension from a given path, appending .pif, .com, .exe, .bat, .lnk,
+    // and .cmd with an assumption the file is a truncated invocable path
+    // (Example: "chrome" referring to "chrome.exe"). ShellExecute will perform
+    // this resolution even if the path refers to a valid file. Chromium
+    // expects paths to be fully qualified and does not need this resolution.
+    if (FAILED(::SHParseDisplayName(path.c_str(), nullptr, &path_id_list,
+                                    SFGAO_FILESYSTEM, nullptr))) {
+      return false;
+    }
+    sei.fMask |= SEE_MASK_IDLIST;
+    sei.lpIDList = path_id_list.m_pData;
+  } else {
+    sei.lpFile = path.c_str();
+  }
+
   // Mitigate the issues caused by loading DLLs on a background thread
   // (http://crbug/973868).
   SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
Loading diff…

Original Bug Report

reported by [email protected]

Chrome (Windows): Opening download “demo. txt” executes sibling “demo. txt.exe” (object-level target confusion / unsafe Shell fallback)


Report description

Chrome (Windows): Opening download “demo. txt” executes sibling “demo. txt.exe” (object-level target confusion / unsafe Shell fallback)


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

chrome://downloads/


The problem

Please describe the technical details of the vulnerability

Summary

On Windows, Chrome’s Open action for a downloaded file can execute a different sibling executable than the file shown/selected in the download UI. Specifically, clicking the download entry displayed as demo. txt results in Chrome creating a process for demo. txt.exe. This is an object-level target confusion / intent-to-execution mismatch (A ≠ B), not a user UI mistake.

Environment

  • Platform: Windows 11 25H2 / Google Chrome Version 144.0.7559.60 (Official Build) (64-bit)
  • Component: Chrome download bubble and chrome://downloads (“Open” action)
  • Directory: C:\Users\potat\Downloads\

Payload

cmd /c “type nul > \?%CD%\demo. txt”

Steps to reproduce

  1. Place two files in the same folder (Downloads):
    • demo. txt (text file)
    • demo. txt.exe (any Windows executable; e.g., a benign PoC that launches Calculator)
  2. Trigger a download entry for demo. txt in Chrome so it appears in the download bubble and in chrome://downloads (e.g., download it from a test server).
  3. In Chrome, click the download entry demo. txt (or click Open) in either:
    • the download bubble, or
    • chrome://downloads.

Observed result

Chrome executes demo. txt.exe instead of opening demo. txt.

ProcMon evidence (process creation)

Process Monitor shows Chrome eventually performing Process Create for:

  • Path: C:\Users\potat\Downloads\demo. txt.exe
  • Command line: "C:\Users\potat\Downloads\demo. txt.exe"

This occurs after Chrome attempts to access ...\demo. txt and fails/gets denied, then falls back and ends up launching the sibling ...\demo. txt.exe.

Object-level proof (NTFS File ID mismatch)

The clicked file (demo. txt) and the executed file (demo. txt.exe) are different NTFS objects:

  • fsutil file queryfileid "C:\Users\potat\Downloads\demo. txt"
    • File ID: 0x00000000000000000080000000001542
  • fsutil file queryfileid "C:\Users\potat\Downloads\demo. txt.exe"
    • File ID: 0x0000000000000000000d0000000018ad

Since the File IDs differ, Chrome’s “Open” action is not bound to the download entry’s file object (A ≠ B).

Expected result

Clicking “Open” for a download entry must open that exact downloaded file object, or fail closed with an error. It must not execute a different file (especially an executable) as a fallback.

Security guidance (Microsoft)

Microsoft’s Windows Shell security guidance explicitly recommends that callers of ShellExecute / ShellExecuteEx provide an unambiguous execution target and use a fully qualified path, rather than relying on the Shell to locate/resolve the file. It states:

  • “Make sure you provide an unambiguous definition of the application that is to be executed.”
  • “When providing the executable file’s path, provide the fully qualified path. Do not depend on the Shell to locate the file.”
  • Searching default locations can be used in spoofing attacks; use a fully qualified path to ensure accessing the desired file.

Reference: https://learn.microsoft.com/en-us/windows/win32/shell/sec-shell

In this case, Chrome’s “Open” action for a download entry should remain bound to the downloaded file object (fail closed on error), rather than delegating to ambiguous Shell resolution that can result in executing a different file.

Impact analysis

This issue provides a download-to-execution phishing primitive on Windows: a victim is presented with a benign-looking Chrome download entry demo. txt and clicks Open (in the download bubble or chrome://downloads), but Chrome can instead execute a different sibling executable demo. txt.exe (A≠B).

RCE is straightforward and does not require complex exploitation: no memory corruption, no sandbox escape, and no multi-step chain. The attacker only needs to place two files in the same download location (demo. txt and an attacker-controlled demo. txt.exe). After a single user action, Chrome launches the attacker-controlled EXE, resulting in arbitrary code execution in the context of the current user. If the user runs Chrome with elevated rights (or is a local administrator), the attacker gains the same privilege level; otherwise it still enables user-level code execution and typical follow-on actions at that privilege level (persistence, credential theft, data exfiltration, lateral movement attempts).

Because the UI indicates the clicked item is a non-executable .txt file while the OS action performed is execution of an .exe, this can be used for high-confidence social engineering/phishing (misleading file-type expectation) and may bypass a user’s normal caution that would apply to explicitly downloaded executables.


The cause

What version of Chrome have you found the security issue in?

Google Chrome Version 144.0.7559.60 (Official Build) (64-bit)

No, it is not related to a crash.

Choose the type of vulnerability

Remote Code Execution (RCE)

How would you like to be publicly acknowledged for your report?

Han Liu (Xi’an Jiaotong University, School of Cyber Science and Engineering)

View on issue tracker