CVE-2025-1915
Overview
Background
- DevTools Protocol (CDP)
- the wire protocol Chrome exposes so clients such as the
chrome.debuggerextension API can inspect and control tabs, including commands likeTarget.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.
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
- Load a debugger extension
An attacker ships an extension holding the
debuggerpermission but without the user granting “Allow access to file URLs”. - Attach a debugging session
The extension calls
chrome.debugger.attachon a tab, establishing an untrusted DevTools client channel whereMayReadLocalFiles()is false. - Issue `Target.createTarget` with a `file:` URL
The extension sends
Target.createTargetwith{url: 'file:///etc/passwd'}, which pre-patchTargetHandler::CreateTargetaccepted since it never checked the file-access grant. - 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
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
| Function | Change | Notes |
|---|---|---|
TargetHandlerchrome/browser/devtools/protocol/target_handler.h |
modified | |
ifchrome/test/data/extensions/api_test/debugger_file_access/background.js |
modified |
Files Changed
AUTHORSchrome/browser/devtools/chrome_devtools_session.ccchrome/browser/devtools/protocol/target_handler.ccchrome/browser/devtools/protocol/target_handler.hchrome/test/data/extensions/api_test/debugger_file_access/background.js
Audit Directions
- Permission-less CDP entry pointsAudit every
protocol::*::Backendcommand reachable by untrusted clients (e.g., intarget_handler.cc,browser_handler.cc) for operations that touchfile:/local resources without consultingMayReadLocalFiles(). - Scheme checks decoupled from capabilityLook 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 gapsVerify 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.
Patch
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.
Regression Test / PoC
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.
Original Bug Report
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:
- Clone the following repository: https://github.com/lgandx/Responder
- Inside the repository, run sudo ./Responder.py -I <INTERFACE> -w, where INTERFACE is the public facing network interface of the server
On the target:
- Download the attached extension
- In background.js, replace SERVER_IP with the address of the server running responder.py
- 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