CVE-2026-79154
Overview
Background
- `DevToolsUIBindings`
- the browser-process class backing the DevTools frontend’s embedder/host API.
- Local vs remote frontend
- a “local” DevTools frontend is served from the trusted
devtools://URL, whereas a remote frontend is loaded elsewhere and is less trusted. - `IsLocalDevToolsFrontendURL`
- a helper that classifies a frontend URL as local (trusted) or not.
- `DispatchHttpRequest`
- a frontend-invokable binding that performs HTTP requests via the browser process.
Root Cause Analysis
abe152dad3eb. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (3431f8c719ae).Pre-fix, several sensitive bindings decided trust dynamically from the web contents’ current URL, or not at all. DispatchHttpRequest (devtools_ui_bindings.cc:1166-1192) performed the request with no local-frontend check whatsoever, so a non-local frontend could invoke it. The file-loading branch in LoadNetworkResource did check, but used web_contents_->GetLastCommittedURL() read at call time (devtools_ui_bindings.cc:1311-1312 GURL frontend_url = web_contents_->GetLastCommittedURL(); if (!IsLocalDevToolsFrontendURL(frontend_url))), which the commit message says could be subject to race conditions (the read and the check are adjacent at 1311-1312, so the race window itself is not visible in the code shown), and EnsureAidaClientAvailable (devtools_ui_bindings.cc:2914-2922) gated only on can_access_aida_ and policy, not on locality.
The fix (commit abe152dad3eb…, chromium/src) adds a cached is_local_frontend_ member (devtools_ui_bindings.h, new bool is_local_frontend_ = false;) computed from IsLocalDevToolsFrontendURL(web_contents_->GetLastCommittedURL()) in the constructor and refreshed in PrimaryPageChanged at navigation commit (added diff). It then gates the sensitive operations on that stored value: DispatchHttpRequest returns a “Request validation failed” error when !is_local_frontend_, the LoadNetworkResource file branch uses if (!is_local_frontend_) instead of the live URL read, and EnsureAidaClientAvailable returns false when !is_local_frontend_ (added diff). This restores the invariant that these privileged bindings are reachable only by a frontend that committed a trusted local URL, and, per the commit message, removes the race by recording locality at commit time.
DispatchHttpRequest) or guarded by a race-prone live-URL read; the fix records whether the committed frontend is local once per navigation and denies those operations to non-local frontends.Attack Path
- Load a non-local DevTools frontend
A remote or otherwise non-
devtools://frontend is connected toDevToolsUIBindings. (inferred) - Invoke an unguarded binding
The frontend calls
DispatchHttpRequest, which pre-fix ran with no locality check (devtools_ui_bindings.cc:1166-1192). - Or win the race
For file loading, have the live
GetLastCommittedURL()read (devtools_ui_bindings.cc:1311-1312) see a local URL while the request comes from a non-local frontend. (inferred: the commit message mentions race conditions, but the read and check are adjacent and the racing sequence is not shown.) - Perform privileged operations
Issue browser-mediated HTTP requests or reach the Aida client from an unauthorized frontend. Loading local
file://resources is possible only through the inferred race in step 3, because the pre-fix check already returned 403 to non-local frontends (devtools_ui_bindings.cc:1310-1327).
Impact Assessment
file:// loading by a non-local frontend was already refused unless --allow-unsafe-devtools-remote-file-loading was set (devtools_ui_bindings.cc:1310-1327). Only the race the commit message mentions could bypass that check (inferred). The advisory rates it medium severity with no in-the-wild exploitation. Concrete escalation beyond those operations is not shown by the material.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/devtools/devtools_ui_bindings.cc |
modified |
Files Changed
chrome/browser/devtools/devtools_ui_bindings.ccchrome/browser/devtools/devtools_ui_bindings.hchrome/browser/devtools/devtools_ui_bindings_unittest.cc
Audit Directions
- Unguarded privileged bindingsEnumerate every frontend-invokable
DevToolsUIBindingsmethod and confirm each sensitive one checksis_local_frontend_. - TOCTOU on trust stateReplace live
GetLastCommittedURL()/ visible-URL trust checks with state captured at navigation commit to avoid races. - Fail-closed defaultsEnsure trust flags like
is_local_frontend_default tofalseand are set only on a verified local commit.
Patch
diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc
index 980b344..0b7bf8c 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings.cc
@@ -873,6 +873,8 @@
->AddObserver(this);
#endif
can_access_aida_ = IsAnyAidaPoweredFeatureEnabled();
+ is_local_frontend_ =
+ IsLocalDevToolsFrontendURL(web_contents_->GetLastCommittedURL());
}
DevToolsUIBindings::~DevToolsUIBindings() {
@@ -1166,6 +1168,14 @@
void DevToolsUIBindings::DispatchHttpRequest(
DispatchCallback callback,
const DevToolsDispatchHttpRequestParams& params) {
+ if (!is_local_frontend_) {
+ base::DictValue response_dict;
+ response_dict.Set("error", "Request validation failed");
+ base::Value response = base::Value(std::move(response_dict));
+ std::move(callback).Run(&response);
+ return;
+ }
+
if (params.stream_id.has_value()) {
int stream_id = *params.stream_id;
auto stream_writer = base::BindRepeating(
@@ -1308,8 +1318,7 @@
NetworkResourceLoader::URLLoaderFactoryHolder url_loader_factory;
if (gurl.SchemeIsFile()) {
- GURL frontend_url = web_contents_->GetLastCommittedURL();
- if (!IsLocalDevToolsFrontendURL(frontend_url)) {
+ if (!is_local_frontend_) {
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAllowUnsafeDevToolsRemoteFileLoading)) {
base::DictValue response_dict;
@@ -2486,9 +2495,10 @@
// Log the frontend location explicitly
GURL frontend_url = web_contents_->GetVisibleURL();
- DevToolsFrontendLocation location = IsLocalDevToolsFrontendURL(frontend_url)
- ? DevToolsFrontendLocation::kLocal
- : DevToolsFrontendLocation::kRemote;
+ DevToolsFrontendLocation location =
+ IsLocalDevToolsFrontendURL(frontend_url)
+ ? DevToolsFrontendLocation::kLocal
+ : DevToolsFrontendLocation::kRemote;
base::UmaHistogramEnumeration("DevTools.FrontendLocation", location);
metrics::structured::StructuredMetricsClient::Record(
@@ -2912,6 +2922,9 @@
}
bool DevToolsUIBindings::EnsureAidaClientAvailable() {
+ if (!is_local_frontend_) {
+ return false;
+ }
if (!can_access_aida_ || AidaClient::CanUseAida(profile_).blocked) {
return false;
}
@@ -3120,6 +3133,8 @@
void DevToolsUIBindings::PrimaryPageChanged() {
frontend_loaded_ = false;
+ is_local_frontend_ =
+ IsLocalDevToolsFrontendURL(web_contents_->GetLastCommittedURL());
}
void DevToolsUIBindings::FrontendLoaded() {
diff --git a/chrome/browser/devtools/devtools_ui_bindings.h b/chrome/browser/devtools/devtools_ui_bindings.h
index 977e1500..f652f24 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.h
+++ b/chrome/browser/devtools/devtools_ui_bindings.h
@@ -479,6 +479,7 @@
std::unique_ptr<DevToolsHttpServiceRegistry> http_service_registry_;
base::UnguessableToken session_id_for_logging_;
+ bool is_local_frontend_ = false;
base::WeakPtrFactory<DevToolsUIBindings> weak_factory_{this};
};
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index a27d55ee..1a37197c 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -432,6 +432,8 @@
base::Unretained(this)));
web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents_, GURL("devtools://devtools/bundled/devtools_app.html"));
bindings_ = std::make_unique<DevToolsUIBindings>(web_contents_);
auto registry = std::make_unique<DevToolsHttpServiceRegistry>();
@@ -826,6 +828,8 @@
base::Unretained(this)));
web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents_, GURL("devtools://devtools/bundled/devtools_app.html"));
test_bindings_ = std::make_unique<TestDevToolsUIBindings>(web_contents_);
auto registry = std::make_unique<DevToolsHttpServiceRegistry>();
Regression Test / PoC
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index a27d55ee..1a37197c 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -432,6 +432,8 @@
base::Unretained(this)));
web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents_, GURL("devtools://devtools/bundled/devtools_app.html"));
bindings_ = std::make_unique<DevToolsUIBindings>(web_contents_);
auto registry = std::make_unique<DevToolsHttpServiceRegistry>();
@@ -826,6 +828,8 @@
base::Unretained(this)));
web_contents_ = web_contents_factory_.CreateWebContents(profile_.get());
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents_, GURL("devtools://devtools/bundled/devtools_app.html"));
test_bindings_ = std::make_unique<TestDevToolsUIBindings>(web_contents_);
auto registry = std::make_unique<DevToolsHttpServiceRegistry>();