Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebAppInstalls
DescriptionInsufficient validation of untrusted input in WebAppInstalls
ComponentWebAppInstalls
Bug ClassLogic Error
Tracker513824891
Fix commit1981552b415b (chromium/src) +20/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
modified

Files Changed

  • chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc
  • chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
From 1981552b415b088fe02e08cc7b98f5820461637c Mon Sep 17 00:00:00 2001
From: Dan Murphy <dmurph@chromium.org>
Date: Tue, 19 May 2026 14:47:38 -0700
Subject: [PATCH] [PWA] Guard against Windows reserved device names

During PWA installation on Windows, the app name is used to create
shortcut (.lnk) and icon files. If the name is a Windows reserved device
name (e.g. COM1, PRN), Windows path normalization redirects file
operations to the physical hardware device or blocks indefinitely. This
allows potential unauthorized device I/O or denial of service via
ThreadPool worker exhaustion.

This CL updates GetSanitizedFileName to check if the file name is a
reserved name on Windows using net::IsReservedNameOnWindows, and
prepends an underscore if it is. This ensures they are treated as
regular files. Unit tests are added to cover these cases.

TAG=agy
CONV=0ebc5617-ab1c-4ea4-b856-1fbbf4177ecd

Bug: 513824891, b:514441287
Change-Id: I812433b58bde9620b04c18707778f4e8bec39e8b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858093
Auto-Submit: Daniel Murphy <dmurph@chromium.org>
Commit-Queue: David Bienvenu <davidbienvenu@chromium.org>
Reviewed-by: David Bienvenu <davidbienvenu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633131}
---

diff --git a/chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc b/chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc
index 8309cd5..8d30580 100644
--- a/chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc
+++ b/chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc
@@ -46,6 +46,7 @@
 #include "chrome/installer/util/util_constants.h"
 #include "content/public/browser/browser_thread.h"
 #include "crypto/obsolete/md5.h"
+#include "net/base/filename_util.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/base/win/shell.h"
 #include "ui/gfx/image/image.h"
@@ -654,6 +655,9 @@
 base::FilePath GetSanitizedFileName(const std::u16string& name) {
   std::wstring file_name = base::AsWString(name);
   base::i18n::ReplaceIllegalCharactersInPath(&file_name, ' ');
+  if (net::IsReservedNameOnWindows(file_name)) {
+    file_name.insert(0, 1, FILE_PATH_LITERAL('_'));
+  }
   return base::FilePath(file_name);
 }
 
diff --git a/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc b/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
index e67cebd0..e2c4d7b 100644
--- a/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
+++ b/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
@@ -103,6 +103,18 @@
             GetSanitizedFileName(u"path/separator"));
   EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_   _")),
             GetSanitizedFileName(u"***"));
+  // Test reserved names on Windows.
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_COM1")),
+            GetSanitizedFileName(u"COM1"));
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_PRN")),
+            GetSanitizedFileName(u"PRN"));
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_CON")),
+            GetSanitizedFileName(u"CON"));
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_LPT9")),
+            GetSanitizedFileName(u"LPT9"));
+  // Test reserved names with extension (only base filename is checked).
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_COM1.lnk")),
+            GetSanitizedFileName(u"COM1.lnk"));
 }
 
 TEST_F(WebAppShortcutWinTest, GetShortcutPaths) {
@@ -451,6 +463,10 @@
   EXPECT_EQ(
       GetIconFilePath(web_app_path, u"***"),
       base::FilePath(FILE_PATH_LITERAL("test\\web\\app\\dir\\_   _.ico")));
+  // Test reserved names on Windows.
+  EXPECT_EQ(
+      GetIconFilePath(web_app_path, u"COM1"),
+      base::FilePath(FILE_PATH_LITERAL("test\\web\\app\\dir\\_COM1.ico")));
 }
 
 }  // namespace web_app::internals
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc b/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
index e67cebd0..e2c4d7b 100644
--- a/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
+++ b/chrome/browser/web_applications/os_integration/web_app_shortcut_win_unittest.cc
@@ -103,6 +103,18 @@
             GetSanitizedFileName(u"path/separator"));
   EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_   _")),
             GetSanitizedFileName(u"***"));
+  // Test reserved names on Windows.
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_COM1")),
+            GetSanitizedFileName(u"COM1"));
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_PRN")),
+            GetSanitizedFileName(u"PRN"));
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_CON")),
+            GetSanitizedFileName(u"CON"));
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_LPT9")),
+            GetSanitizedFileName(u"LPT9"));
+  // Test reserved names with extension (only base filename is checked).
+  EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("_COM1.lnk")),
+            GetSanitizedFileName(u"COM1.lnk"));
 }
 
 TEST_F(WebAppShortcutWinTest, GetShortcutPaths) {
@@ -451,6 +463,10 @@
   EXPECT_EQ(
       GetIconFilePath(web_app_path, u"***"),
       base::FilePath(FILE_PATH_LITERAL("test\\web\\app\\dir\\_   _.ico")));
+  // Test reserved names on Windows.
+  EXPECT_EQ(
+      GetIconFilePath(web_app_path, u"COM1"),
+      base::FilePath(FILE_PATH_LITERAL("test\\web\\app\\dir\\_COM1.ico")));
 }
 
 }  // namespace web_app::internals
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential hijack of Windows reserved device names via PWA installation

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The PWA installation process on Windows fails to sanitize reserved device names (e.g., COM1, PRN) in the web app name. This allows an attacker to potentially perform unauthorized hardware I/O or cause a denial of service in the browser process.

Affected files:

  • chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc

Estimated timestamp from git blame: 2012-02-11

Summary

The PWA shortcut and icon creation logic on Windows does not check for reserved device names when converting a web app manifest’s name into a filename. While it strips illegal characters (like * or ?), it fails to reject or transform names such as CON, PRN, AUX, NUL, COM1-9, and LPT1-9. This can be leveraged by a malicious website to interact with hardware devices via the browser process or cause a persistent denial of service (DoS) by hanging background worker threads.

Root Cause Analysis

The issue is located in the GetSanitizedFileName function within chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc.

// chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc:654
base::FilePath GetSanitizedFileName(const std::u16string& name) {
  std::wstring file_name = base::AsWString(name);
  base::i18n::ReplaceIllegalCharactersInPath(&file_name, ' ');
  return base::FilePath(file_name);
}

The call to base::i18n::ReplaceIllegalCharactersInPath removes common illegal characters but does not account for reserved names. Other components in the PWA subsystem correctly use net::IsReservedNameOnWindows to mitigate this risk (e.g., in chrome/browser/web_applications/os_integration/web_app_handler_registration_utils_win.cc), but this check is missing from the primary shortcut and icon generation paths.

Potential Impact

  1. Unauthorized Device I/O: During PWA installation, the browser attempts to create a shortcut (e.g., Desktop\COM1.lnk). Due to legacy Windows path normalization, writing to COM1.lnk redirects the binary data of the shortcut to the COM1 serial port. An attacker can control the manifest’s description field, which is embedded in the shortcut file, potentially allowing them to send arbitrary commands to a serial device (e.g., a printer or industrial controller) without the user’s consent, bypassing WebSerial permissions.
  2. ThreadPool Denial of Service: The browser also manages icon files (e.g., COM1.ico) and their associated checksums (e.g., COM1.ico.md5). During icon update checks, the browser calls base::ReadFile on the checksum file. If COM1 refers to a blocking serial device, the ::ReadFile call on the background ThreadPool worker thread will hang indefinitely. Repeated triggers can exhaust the browser’s thread pool, leading to a complete hang of background tasks.

Suggested Potential Steps to Reproduce

  1. On a Windows 10 system, host a web manifest where the "name" field is set to a reserved name like "COM1" and the "description" field contains a payload.
  2. In Chrome, navigate to the site and install the PWA.
  3. Observe that the browser process attempts to create a file at Desktop\COM1.lnk. If a serial listener is active on COM1, it may receive binary data during the installation.
  4. Trigger a PWA update or icon refresh. Observe the browser’s background thread pool for worker threads hanging in a base::ReadFile operation on the COM1.ico.md5 file.

Update GetSanitizedFileName in chrome/browser/web_applications/os_integration/web_app_shortcut_win.cc to include a check for reserved names using net::IsReservedNameOnWindows. If a reserved name is detected, it should be transformed (e.g., by prepending an underscore) to ensure it is treated as a regular file rather than a device handle.

Note: These findings are based on code analysis; our tooling does not currently have the capability to execute code or provide a working proof-of-concept.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker