CVE-2026-10922
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/devtools/devtools_ui_bindings.cc |
modified | |
DevToolsHttpServiceHandlerchrome/browser/devtools/devtools_ui_bindings.h |
modified | |
DevToolsHttpServiceRegistrychrome/browser/devtools/devtools_ui_bindings.h |
modified | |
DevToolsUIBindingsDispatchHttpRequestTestchrome/browser/devtools/devtools_ui_bindings.h |
modified | |
DevToolsUIBindingsLoadNetworkResourceTestchrome/browser/devtools/devtools_ui_bindings.h |
modified | |
PortForwardingStatusSerializerchrome/browser/devtools/devtools_ui_bindings.h |
modified | |
Profilechrome/browser/devtools/devtools_ui_bindings.h |
modified | |
Delegatechrome/browser/devtools/devtools_ui_bindings.h |
modified | |
DevToolsUIBindingsTestchrome/browser/devtools/devtools_ui_bindings_unittest.cc |
modified | |
DevToolsUIBindingsLoadNetworkResourceTestchrome/browser/devtools/devtools_ui_bindings_unittest.cc |
modified | |
MockDevToolsUIBindingsDelegatechrome/browser/devtools/devtools_ui_bindings_unittest.cc |
modified |
Files Changed
chrome/browser/devtools/devtools_ui_bindings.ccchrome/browser/devtools/devtools_ui_bindings.hchrome/browser/devtools/devtools_ui_bindings_unittest.cc
Patch
From 8e5202fd839ec1cd9c2d40a51386c1d4382a178a Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Wed, 22 Apr 2026 02:35:45 -0700
Subject: [PATCH] Restrict file:// resource loading from remote DevTools frontends.
This CL addresses a security vulnerability where an attacker could
force Chrome to load a historical, vulnerable DevTools frontend from
the official Appspot CDN and "resurrect" previously patched XSS bugs
to read arbitrary local files via InspectorFrontendHost's
`loadNetworkResource`.
To mitigate this, the security enforcement is moved to the C++ browser
process with the following changes:
1. `DevToolsUIBindings::LoadNetworkResource` now blocks
resource loads if the frontend is remote, unless the inspected target
is also a local origin or an explicit command-line flag is set.
2. Added the `--allow-unsafe-devtools-remote-file-loading` command line switch as an
escape hatch for developers who legitimately need to load local
source maps for remote-origin sites.
3. Plumbed a 403 error with a `messageOverride` through
`DevToolsUIBindings::LoadNetworkResource` so the DevTools frontend
can surface a helpful warning to the user when a load is blocked.
Bug: 499164652
Change-Id: Ib29eedd72eb79ec85cf9786c0fe59e2d78f277e1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7780720
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618738}
---
diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc
index 3108dd3..431f356 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings.cc
@@ -1265,8 +1265,32 @@
resource_request.site_for_cookies = net::SiteForCookies::FromUrl(gurl);
resource_request.headers.AddHeadersFromString(headers);
+ content::WebContents* target_tab = delegate_->GetInspectedWebContents();
+
NetworkResourceLoader::URLLoaderFactoryHolder url_loader_factory;
if (gurl.SchemeIsFile()) {
+ GURL frontend_url = web_contents_->GetLastCommittedURL();
+ bool is_remote_frontend =
+ frontend_url.is_valid() && !frontend_url.IsAboutBlank() &&
+ IsValidRemoteFrontendURL(frontend_url);
+ if (is_remote_frontend) {
+ if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kAllowUnsafeDevToolsRemoteFileLoading)) {
+ base::DictValue response_dict;
+ response_dict.Set("statusCode", 403);
+ response_dict.Set("netError", net::ERR_ACCESS_DENIED);
+ response_dict.Set("netErrorName",
+ net::ErrorToString(net::ERR_ACCESS_DENIED));
+ response_dict.Set(
+ "messageOverride",
+ "Local file loading is restricted for remote DevTools. Use "
+ "--allow-unsafe-devtools-remote-file-loading to enable it.");
+ auto response = base::Value(std::move(response_dict));
+ std::move(callback).Run(&response);
+ return;
+ }
+ }
+
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
content::CreateFileURLLoaderFactory(
base::FilePath() /* profile_path */,
@@ -1275,7 +1299,6 @@
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
std::move(pending_remote)));
} else if (content::HasWebUIScheme(gurl)) {
- content::WebContents* target_tab = delegate_->GetInspectedWebContents();
#if defined(NDEBUG)
// In release builds, allow files from the chrome://, devtools:// and
// chrome-untrusted:// schemes if a custom devtools front-end was specified.
@@ -1314,7 +1337,6 @@
return;
}
} else {
- content::WebContents* target_tab = delegate_->GetInspectedWebContents();
if (target_tab) {
auto* partition =
target_tab->GetPrimaryMainFrame()->GetStoragePartition();
diff --git a/chrome/browser/devtools/devtools_ui_bindings.h b/chrome/browser/devtools/devtools_ui_bindings.h
index 61225d6..f3bb7326 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.h
+++ b/chrome/browser/devtools/devtools_ui_bindings.h
@@ -55,6 +55,7 @@
class DevToolsHttpServiceHandler;
class DevToolsHttpServiceRegistry;
class DevToolsUIBindingsDispatchHttpRequestTest;
+class DevToolsUIBindingsLoadNetworkResourceTest;
class PortForwardingStatusSerializer;
class Profile;
@@ -68,6 +69,7 @@
public DevToolsFileHelper::Delegate {
friend class DevToolsUIBindingsDispatchHttpRequestTest;
friend class DevToolsUIBindingsDispatchHttpRequestStreamingTest;
+ friend class DevToolsUIBindingsLoadNetworkResourceTest;
public:
class Delegate {
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index bb15d792..cde1fa4 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -10,6 +10,7 @@
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
+#include "base/test/scoped_command_line.h"
#include "base/test/test_future.h"
#include "chrome/browser/devtools/devtools_dispatch_http_request_params.h"
#include "chrome/browser/devtools/devtools_http_service_handler.h"
@@ -17,12 +18,15 @@
#include "chrome/browser/devtools/features.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/browser/sync/sync_service_factory.h"
+#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/sync/test/test_sync_service.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/url_loader_interceptor.h"
#include "google_apis/gaia/google_service_auth_error.h"
@@ -36,6 +40,183 @@
class DevToolsUIBindingsTest : public testing::Test {};
+class DevToolsUIBindingsLoadNetworkResourceTest : public testing::Test {
+ public:
+ void SetUp() override {
+ profile_ = std::make_unique<TestingProfile>();
+ web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
+ bindings_ = std::make_unique<DevToolsUIBindings>(web_contents_);
+ }
+
+ content::WebContents* web_contents() { return web_contents_; }
+ DevToolsUIBindings* bindings() { return bindings_.get(); }
+
+ void CallLoadNetworkResource(const std::string& url,
+ const std::string& headers,
+ int stream_id,
+ DevToolsUIBindings::DispatchCallback callback) {
+ bindings_->LoadNetworkResource(std::move(callback), url, headers,
+ stream_id);
+ }
+
+ protected:
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<TestingProfile> profile_;
+ content::TestWebContentsFactory web_contents_factory_;
+ raw_ptr<content::WebContents> web_contents_;
+ std::unique_ptr<DevToolsUIBindings> bindings_;
+};
+
+class MockDevToolsUIBindingsDelegate : public DevToolsUIBindings::Delegate {
+ public:
+ explicit MockDevToolsUIBindingsDelegate(
+ content::WebContents* inspected_web_contents)
+ : inspected_web_contents_(inspected_web_contents) {}
+
+ content::WebContents* GetInspectedWebContents() override {
+ return inspected_web_contents_;
+ }
+ void ActivateWindow() override {}
+ void CloseWindow() override {}
+ void Inspect(scoped_refptr<content::DevToolsAgentHost> host) override {}
+ void SetInspectedPageBounds(const gfx::Rect& rect) override {}
+ void InspectElementCompleted() override {}
+ void SetIsDocked(bool is_docked) override {}
+ void OpenInNewTab(const std::string& url) override {}
+ void OpenSearchResultsInNewTab(const std::string& query) override {}
+ void SetWhitelistedShortcuts(const std::string& message) override {}
+ void SetEyeDropperActive(bool active) override {}
+ void OpenNodeFrontend() override {}
+ void InspectedContentsClosing() override {}
+ void OnLoadCompleted() override {}
+ void ReadyForTest() override {}
+ void ConnectionReady() override {}
+ void SetOpenNewWindowForPopups(bool value) override {}
+ infobars::ContentInfoBarManager* GetInfoBarManager() override {
+ return nullptr;
+ }
+ void RenderProcessGone(bool crashed) override {}
+ void ShowCertificateViewer(const std::string& cert_chain) override {}
+ int GetDockStateForLogging() override { return 0; }
+ int GetOpenedByForLogging() override { return 0; }
+ int GetClosedByForLogging() override { return 0; }
+
+ private:
+ raw_ptr<content::WebContents> inspected_web_contents_;
Regression Test / PoC
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index bb15d792..cde1fa4 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -10,6 +10,7 @@
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
+#include "base/test/scoped_command_line.h"
#include "base/test/test_future.h"
#include "chrome/browser/devtools/devtools_dispatch_http_request_params.h"
#include "chrome/browser/devtools/devtools_http_service_handler.h"
@@ -17,12 +18,15 @@
#include "chrome/browser/devtools/features.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/browser/sync/sync_service_factory.h"
+#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/sync/test/test_sync_service.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/url_loader_interceptor.h"
#include "google_apis/gaia/google_service_auth_error.h"
@@ -36,6 +40,183 @@
class DevToolsUIBindingsTest : public testing::Test {};
+class DevToolsUIBindingsLoadNetworkResourceTest : public testing::Test {
+ public:
+ void SetUp() override {
+ profile_ = std::make_unique<TestingProfile>();
+ web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
+ bindings_ = std::make_unique<DevToolsUIBindings>(web_contents_);
+ }
+
+ content::WebContents* web_contents() { return web_contents_; }
+ DevToolsUIBindings* bindings() { return bindings_.get(); }
+
+ void CallLoadNetworkResource(const std::string& url,
+ const std::string& headers,
+ int stream_id,
+ DevToolsUIBindings::DispatchCallback callback) {
+ bindings_->LoadNetworkResource(std::move(callback), url, headers,
+ stream_id);
+ }
+
+ protected:
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<TestingProfile> profile_;
+ content::TestWebContentsFactory web_contents_factory_;
+ raw_ptr<content::WebContents> web_contents_;
+ std::unique_ptr<DevToolsUIBindings> bindings_;
+};
+
+class MockDevToolsUIBindingsDelegate : public DevToolsUIBindings::Delegate {
+ public:
+ explicit MockDevToolsUIBindingsDelegate(
+ content::WebContents* inspected_web_contents)
+ : inspected_web_contents_(inspected_web_contents) {}
+
+ content::WebContents* GetInspectedWebContents() override {
+ return inspected_web_contents_;
+ }
+ void ActivateWindow() override {}
+ void CloseWindow() override {}
+ void Inspect(scoped_refptr<content::DevToolsAgentHost> host) override {}
+ void SetInspectedPageBounds(const gfx::Rect& rect) override {}
+ void InspectElementCompleted() override {}
+ void SetIsDocked(bool is_docked) override {}
+ void OpenInNewTab(const std::string& url) override {}
+ void OpenSearchResultsInNewTab(const std::string& query) override {}
+ void SetWhitelistedShortcuts(const std::string& message) override {}
+ void SetEyeDropperActive(bool active) override {}
+ void OpenNodeFrontend() override {}
+ void InspectedContentsClosing() override {}
+ void OnLoadCompleted() override {}
+ void ReadyForTest() override {}
+ void ConnectionReady() override {}
+ void SetOpenNewWindowForPopups(bool value) override {}
+ infobars::ContentInfoBarManager* GetInfoBarManager() override {
+ return nullptr;
+ }
+ void RenderProcessGone(bool crashed) override {}
+ void ShowCertificateViewer(const std::string& cert_chain) override {}
+ int GetDockStateForLogging() override { return 0; }
+ int GetOpenedByForLogging() override { return 0; }
+ int GetClosedByForLogging() override { return 0; }
+
+ private:
+ raw_ptr<content::WebContents> inspected_web_contents_;
+};
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+ BlocksFileSchemeFromRemoteFrontend) {
+ // Simulate a remote frontend URL.
+ GURL remote_url(
+ "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
+ "inspector.html");
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+ remote_url);
+
+ base::RunLoop run_loop;
+ base::DictValue result;
+
+ CallLoadNetworkResource(
+ "file:///etc/passwd", "", 0,
+ base::BindLambdaForTesting([&](const base::Value* value) {
+ result = value->GetDict().Clone();
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+
+ EXPECT_EQ(result.FindInt("statusCode"), 403);
+ ASSERT_NE(result.FindString("messageOverride"), nullptr);
+ EXPECT_EQ(*result.FindString("messageOverride"),
+ "Local file loading is restricted for remote DevTools. Use "
+ "--allow-unsafe-devtools-remote-file-loading to enable it.");
+}
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+ AllowsFileSchemeFromRemoteFrontendWithFlag) {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitch(
+ switches::kAllowUnsafeDevToolsRemoteFileLoading);
+
+ GURL remote_url(
+ "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
+ "inspector.html");
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+ remote_url);
+
+ base::RunLoop run_loop;
+ base::DictValue result;
+
+ CallLoadNetworkResource(
+ "file:///etc/passwd", "", 0,
+ base::BindLambdaForTesting([&](const base::Value* value) {
+ result = value->GetDict().Clone();
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+
+ auto* msg = result.FindString("messageOverride");
+ EXPECT_EQ(msg, nullptr);
+ EXPECT_NE(result.FindInt("statusCode"), 403);
+}
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+ AllowsFileSchemeFromLocalFrontend) {
+ GURL local_url("devtools://devtools/bundled/devtools_app.html");
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+ local_url);
+
+ base::RunLoop run_loop;
+ base::DictValue result;
+
+ CallLoadNetworkResource(
+ "file:///etc/passwd", "", 0,
+ base::BindLambdaForTesting([&](const base::Value* value) {
+ result = value->GetDict().Clone();
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+
+ auto* msg = result.FindString("messageOverride");
+ EXPECT_EQ(msg, nullptr);
+ EXPECT_NE(result.FindInt("statusCode"), 403);
+}
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+ BlocksFileSchemeFromRemoteFrontendWithLocalTarget) {
+ GURL remote_url(
+ "https://chrome-devtools-frontend.appspot.com/serve_rev/@12345/"
+ "inspector.html");
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+ remote_url);
+
+ content::WebContents* inspected_web_contents =
+ web_contents_factory_.CreateWebContents(profile_.get());
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ inspected_web_contents, GURL("file:///tmp/index.html"));
+
+ auto delegate =
+ std::make_unique<MockDevToolsUIBindingsDelegate>(inspected_web_contents);
+ bindings()->SetDelegate(delegate.release());
+
+ base::RunLoop run_loop;
+ base::DictValue result;
+
+ CallLoadNetworkResource(
+ "file:///etc/passwd", "", 0,
+ base::BindLambdaForTesting([&](const base::Value* value) {
+ result = value->GetDict().Clone();
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+
+ EXPECT_EQ(result.FindInt("statusCode"), 403);
+ ASSERT_NE(result.FindString("messageOverride"), nullptr);
+ EXPECT_EQ(*result.FindString("messageOverride"),
+ "Local file loading is restricted for remote DevTools. Use "
+ "--allow-unsafe-devtools-remote-file-loading to enable it.");
+}
+
TEST_F(DevToolsUIBindingsTest, SanitizeFrontendURL) {
std::vector<std::pair<std::string, std::string>> tests = {
{"random-string", "devtools://devtools/"},
Original Bug Report
Local file disclosure via loading historical DevTools frontends
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 without the security team.
Overview: Insufficient path validation in DevTools URL handling allows a malicious debugging target to force Chrome to load a historical, vulnerable DevTools frontend from the official CDN. An attacker can exploit resurrected JavaScript logic bugs in older frontends to read arbitrary local files via InspectorFrontendHost.loadNetworkResource.
Affected files:
chrome/browser/devtools/devtools_ui_bindings.ccchrome/browser/devtools/devtools_window.ccchrome/browser/devtools/devtools_ui.ccchrome/browser/devtools/devtools_ui_data_source.cc
Estimated timestamp from git blame: 2025-10-01
Summary
When handling remote debugging targets, Chrome relies on the target-provided devtoolsFrontendUrl to load the appropriate DevTools frontend. While the browser correctly restricts the host to chrome-devtools-frontend.appspot.com (via DevToolsUI::GetProxyURL), it fails to sufficiently validate the path component of the URL.
Functions such as SanitizeFrontendPath and DevToolsUIBindings::IsValidRemoteFrontendURL only enforce a character whitelist (allowing alphanumeric characters, /, -, _, ., and @) and do not enforce an entrypoint file allowlist (such as strictly requiring devtools.html or inspector.html).
This oversight allows an attacker to supply a devtoolsFrontendUrl pointing to a historical revision of the DevTools frontend hosted on the official Appspot CDN. By resurrecting an older frontend version, an attacker can bypass modern JavaScript security patches. For example, older frontends lacked robust validation for file:// URLs in their loadNetworkResource implementation. Because the C++ backend (DevToolsUIBindings::LoadNetworkResource) creates a FileURLLoaderFactory for file:// URLs without enforcing its own path or origin restrictions, it relies entirely on the frontend JavaScript for security. Loading a vulnerable historical frontend thus re-introduces the ability to read arbitrary local files from the user’s system.
Potential Attack Steps
Note: These are suggested steps based on code analysis; a working proof of concept has not been executed.
- Attacker Setup: The attacker sets up a malicious remote debugging target, reachable via ADB or TCP target discovery.
- Malicious Payload: The target responds to the
/json/listdiscovery request with a payload wheredevtoolsFrontendUrlpoints to a known vulnerable historical revision on the Appspot CDN (e.g.,https://chrome-devtools-frontend.appspot.com/serve_rev/@<vulnerable_hash>/some_vulnerable_file.html). - User Interaction: The victim user navigates to
chrome://inspectand clicks the ‘inspect’ link for the malicious target. - Bypassing Validation: Chrome proxies the URL to
devtools://devtools/remote/serve_rev/@<hash>/some_vulnerable_file.html. The path passes validation inSanitizeFrontendPathandIsValidRemoteFrontendURLbecause it only contains allowed characters, despite not being a standard entrypoint. - Exploitation: The historical frontend is loaded with full embedder bindings. The outdated JavaScript executes and calls the privileged
InspectorFrontendHost.loadNetworkResourceAPI, requesting an arbitrary local file (e.g.,file:///etc/passwd). - Data Exfiltration: The C++ backend
DevToolsUIBindings::LoadNetworkResourceprocesses thefile://request unconditionally, reading the file and returning its contents to the attacker’s JavaScript, which then exfiltrates the data.
Suggested Fix
Enforce a strict entrypoint allowlist for the filename portion of remote DevTools URLs.
DevToolsUIBindings::IsValidRemoteFrontendURL or SanitizeFrontendPath should verify that the requested file is an approved DevTools entrypoint (e.g., devtools.html, inspector.html, worker_app.html, js_app.html, or node_app.html), similar to the logic already present in SanitizeRemoteFrontendURL for handling the remoteFrontendUrl query parameter.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.