Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper Limitation of a Pathname to a Restricted Directory in DevTools
DescriptionImproper Limitation of a Pathname to a Restricted Directory in DevTools
ComponentDevTools
Sub-componentPlatform>Extensions
Bug ClassLogic Error
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.
Tracker391114799
Fix commite166400fa4c6 (chromium/src) +39/-4
CWECWE-22
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedTopi Lassila
Reported2025-01-20 · bounty $4000 (per vendor advisory)
Fix landed2025-02-03 (public git)
Disclosed2025-03-04
Patch gap29 days — fix public in source before the advisory

Background

DevTools Protocol (CDP)
the wire protocol Chrome exposes so clients such as the chrome.debugger extension API can inspect and control tabs, including commands like Target.createTarget.
Trusted vs. untrusted client
a DevTools client is “trusted” when it is a first-party browser UI, while extension-driven sessions are untrusted and gated per-domain via IsDomainAvailableToUntrustedClient.
`MayReadLocalFiles`
a per-client capability on the DevTools channel client indicating whether that client (e.g., an extension) has been granted “Allow access to file URLs”.
`file:` scheme
a URL scheme (gurl.SchemeIsFile()) that resolves to the local filesystem rather than a network resource, which is why navigating to one grants read access to local files.

Root Cause Analysis

TargetHandler::CreateTarget validated the requested URL against network::IsURLPotentiallyTrustworthy-style checks but never consulted whether the requesting client was permitted to read local files. Because Target.createTarget was reachable by untrusted extension clients that had not been granted file-URL access, an extension could pass a file:// URL and cause the browser to open a target navigating to the local filesystem. The invariant violated is that a client’s local-file read permission (MayReadLocalFiles) must gate any operation that can load file: content; CreateTarget bypassed that gate entirely.

The fix threads MayReadLocalFiles() from the channel client through ChromeDevToolsSession into the TargetHandler constructor, stores it as may_read_local_files_, and adds an early rejection in CreateTarget that returns a ServerError when !may_read_local_files_ && gurl.SchemeIsFile(). This works because the permission decision now happens at the exact command entry point, before any target/window is created.

Key insight
The single mistake was authorizing Target.createTarget purely on domain availability and URL trustworthiness while ignoring the client’s file-access grant, letting an extension without file-URL permission navigate a new target to a file: URL. The fix closes it by explicitly denying file:-scheme targets whenever the client’s may_read_local_files_ flag is false.

Attack Path

  1. Load a debugger extension An attacker ships an extension holding the debugger permission but without the user granting “Allow access to file URLs”.
  2. Attach a debugging session The extension calls chrome.debugger.attach on a tab, establishing an untrusted DevTools client channel where MayReadLocalFiles() is false.
  3. Issue `Target.createTarget` with a `file:` URL The extension sends Target.createTarget with {url: 'file:///etc/passwd'}, which pre-patch TargetHandler::CreateTarget accepted since it never checked the file-access grant.
  4. Read local file contents The new target navigates to the local file, and the extension inspects the rendered/scriptable content via the debugger session, exfiltrating data outside the profile’s restricted directory.

Impact Assessment

An attacker-controlled extension gains read access to arbitrary local files (file: URLs) from the browser process context, escaping the intended pathname restriction, despite the user never granting file-URL access. The precondition is an installed extension with the debugger permission and the ability to call Target.createTarget; no separate file-access grant is needed pre-patch, which is precisely the privilege boundary the bug crosses.

Changed Functions

FunctionChangeNotes
TargetHandler
chrome/browser/devtools/protocol/target_handler.h
modified
if
chrome/test/data/extensions/api_test/debugger_file_access/background.js
modified

Files Changed

  • AUTHORS
  • chrome/browser/devtools/chrome_devtools_session.cc
  • chrome/browser/devtools/protocol/target_handler.cc
  • chrome/browser/devtools/protocol/target_handler.h
  • chrome/test/data/extensions/api_test/debugger_file_access/background.js

Audit Directions

  • Permission-less CDP entry points
    Audit every protocol::*::Backend command reachable by untrusted clients (e.g., in target_handler.cc, browser_handler.cc) for operations that touch file:/local resources without consulting MayReadLocalFiles().
  • Scheme checks decoupled from capability
    Look for URL validation that only inspects trustworthiness or domain availability (SchemeIsFile, IsURLPotentiallyTrustworthy) but omits the client’s granted permissions, especially where new targets, downloads, or navigations are initiated.
  • Capability propagation gaps
    Verify that client capability flags surfaced on the DevTools channel client are actually threaded into each handler constructor and enforced, since a flag that is read in one handler but silently dropped in another reintroduces the same bypass.
From e166400fa4c632c5150d75bf587e8a34285fb783 Mon Sep 17 00:00:00 2001
From: Topi Lassila <tolassila@gmail.com>
Date: Mon, 03 Feb 2025 00:09:25 -0800
Subject: [PATCH] Check whether devtools clients should be allowed to target local files

Bug: 391114799
Change-Id: I1ab514ebe7b4a00c740bb1943dcf3c3401d931b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6218796
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1414781}
---

diff --git a/AUTHORS b/AUTHORS
index a3c3351..e96a3af 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1467,6 +1467,7 @@
 Tomas Popela <tomas.popela@gmail.com>
 Tomasz Edward Posłuszny <tom@devpeer.net>
 Tony Shen <legendmastertony@gmail.com>
+Topi Lassila <tolassila@gmail.com>
 Torsten Kurbad <google@tk-webart.de>
 Toshihito Kikuchi <leamovret@gmail.com>
 Toshiaki Tanaka <zokutyou2@gmail.com>
diff --git a/chrome/browser/devtools/chrome_devtools_session.cc b/chrome/browser/devtools/chrome_devtools_session.cc
index a5ae0dcfd..17d7ac9 100644
--- a/chrome/browser/devtools/chrome_devtools_session.cc
+++ b/chrome/browser/devtools/chrome_devtools_session.cc
@@ -100,7 +100,8 @@
   if (IsDomainAvailableToUntrustedClient<TargetHandler>() ||
       channel->GetClient()->IsTrusted()) {
     target_handler_ = std::make_unique<TargetHandler>(
-        &dispatcher_, channel->GetClient()->IsTrusted());
+        &dispatcher_, channel->GetClient()->IsTrusted(),
+        channel->GetClient()->MayReadLocalFiles());
   }
   if (IsDomainAvailableToUntrustedClient<BrowserHandler>() ||
       channel->GetClient()->IsTrusted()) {
diff --git a/chrome/browser/devtools/protocol/target_handler.cc b/chrome/browser/devtools/protocol/target_handler.cc
index 8087bff4..363ee4f 100644
--- a/chrome/browser/devtools/protocol/target_handler.cc
+++ b/chrome/browser/devtools/protocol/target_handler.cc
@@ -48,8 +48,9 @@
 }  // namespace
 
 TargetHandler::TargetHandler(protocol::UberDispatcher* dispatcher,
-                             bool is_trusted)
-    : is_trusted_(is_trusted) {
+                             bool is_trusted,
+                             bool may_read_local_files)
+    : is_trusted_(is_trusted), may_read_local_files_(may_read_local_files) {
   protocol::Target::Dispatcher::wire(dispatcher, this);
 }
 
@@ -140,6 +141,11 @@
         "Refusing to create a target with the specified URL");
   }
 
+  if (!may_read_local_files_ && gurl.SchemeIsFile()) {
+    return protocol::Response::ServerError(
+        "Creating a target with a local URL is not allowed");
+  }
+
   create_new_window = !target_browser;
 
   const bool set_window_position = left || top || width || height;
diff --git a/chrome/browser/devtools/protocol/target_handler.h b/chrome/browser/devtools/protocol/target_handler.h
index 67cddb8..0e4b0b3 100644
--- a/chrome/browser/devtools/protocol/target_handler.h
+++ b/chrome/browser/devtools/protocol/target_handler.h
@@ -16,7 +16,9 @@
 
 class TargetHandler : public protocol::Target::Backend {
  public:
-  TargetHandler(protocol::UberDispatcher* dispatcher, bool is_trusted);
+  TargetHandler(protocol::UberDispatcher* dispatcher,
+                bool is_trusted,
+                bool may_read_local_files);
 
   TargetHandler(const TargetHandler&) = delete;
   TargetHandler& operator=(const TargetHandler&) = delete;
@@ -46,6 +48,7 @@
  private:
   RemoteLocations remote_locations_;
   const bool is_trusted_;
+  const bool may_read_local_files_;
 };
 
 #endif  // CHROME_BROWSER_DEVTOOLS_PROTOCOL_TARGET_HANDLER_H_
diff --git a/chrome/test/data/extensions/api_test/debugger_file_access/background.js b/chrome/test/data/extensions/api_test/debugger_file_access/background.js
index 36a064c3..38e3f00 100644
--- a/chrome/test/data/extensions/api_test/debugger_file_access/background.js
+++ b/chrome/test/data/extensions/api_test/debugger_file_access/background.js
@@ -118,6 +118,30 @@
       });
     },
 
+    function testCreateTarget() {
+      const url = chrome.runtime.getURL('dummy.html');
+      openTab(url).then((tab) => {
+        chrome.test.assertEq(url, tab.url);
+        const tabId = tab.id;
+        chrome.debugger.attach({tabId: tabId}, '1.1', function() {
+          chrome.test.assertNoLastError();
+          chrome.debugger.sendCommand({tabId: tabId}, 'Target.createTarget',
+                                      {url: fileUrl}, function() {
+            if (expectFileAccess) {
+              chrome.test.assertNoLastError();
+            } else {
+              chrome.test.assertLastError(JSON.stringify({
+                code: -32000,
+                message: 'Creating a target with a local URL is not allowed'
+              }));
+            }
+            chrome.tabs.remove(tabId);
+            chrome.test.succeed();
+          });
+        });
+      });
+    },
+
     // https://crbug.com/866426
     function setDownloadBehavior() {
       // We never allow to write local files.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/test/data/extensions/api_test/debugger_file_access/background.js b/chrome/test/data/extensions/api_test/debugger_file_access/background.js
index 36a064c3..38e3f00 100644
--- a/chrome/test/data/extensions/api_test/debugger_file_access/background.js
+++ b/chrome/test/data/extensions/api_test/debugger_file_access/background.js
@@ -118,6 +118,30 @@
       });
     },
 
+    function testCreateTarget() {
+      const url = chrome.runtime.getURL('dummy.html');
+      openTab(url).then((tab) => {
+        chrome.test.assertEq(url, tab.url);
+        const tabId = tab.id;
+        chrome.debugger.attach({tabId: tabId}, '1.1', function() {
+          chrome.test.assertNoLastError();
+          chrome.debugger.sendCommand({tabId: tabId}, 'Target.createTarget',
+                                      {url: fileUrl}, function() {
+            if (expectFileAccess) {
+              chrome.test.assertNoLastError();
+            } else {
+              chrome.test.assertLastError(JSON.stringify({
+                code: -32000,
+                message: 'Creating a target with a local URL is not allowed'
+              }));
+            }
+            chrome.tabs.remove(tabId);
+            chrome.test.succeed();
+          });
+        });
+      });
+    },
+
     // https://crbug.com/866426
     function setDownloadBehavior() {
       // We never allow to write local files.
Loading diff…

Original Bug Report

reported by ba...@gmail.com

Extensions without file URL access can open UNC paths through chrome.debugger

Steps to reproduce the problem

On a server that the target can access:

  1. Clone the following repository: https://github.com/lgandx/Responder
  2. Inside the repository, run sudo ./Responder.py -I <INTERFACE> -w, where INTERFACE is the public facing network interface of the server

On the target:

  1. Download the attached extension
  2. In background.js, replace SERVER_IP with the address of the server running responder.py
  3. Install the extension, and make sure to disable “Allow access to file URLs”

Note that the NTLM hash is leaked twice, once after the initial install with access to file URLs enabled (as expected), but also when the extension reloads after disabling that option.

Problem Description

DevTool’s TargetHandler::CreateTarget does not check if the current extension should be allowed to access local files, which allows extensions with the debugger permission to open arbitrary file URLs. On Windows, this can be exploited by opening a UNC path, which will then lead to the current user’s NTLM hash getting leaked, as in bugs such as https://issues.chromium.org/issues/40060207

In addition to the extension that can be used to reproduce this, I’ve also attached a suggested fix, which should disallow creating targets with file URLs if the appropriate permissions aren’t given.

Summary

Extensions without file URL access can open UNC paths through chrome.debugger

Additional Data

Category: Security
Chrome Channel: Stable
Regression: No

View on issue tracker