Medium watch ✔︎ verified CVSS 6.5 chrome Authorization Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in DevTools
DescriptionMissing authorization in DevTools
ComponentDevTools
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Citations check out. Minor overstatement: Impact implies remote frontends could load file:// pre-fix, but lines 1310-1327 already blocked that; only the inferred TOCTOU path supports it. Read/use at 1311-1312 are adjacent, so the 'gap' is commit-message-derived.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker520492291
Fix commitabe152dad3eb (chromium/src) +25/-5
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-08-12 (public git)
Disclosed2026-08-25
Patch gap13 days — fix public in source before the advisory

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

Re-analyzed on 2026-10-03 against the actual fix commit 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.

Key insight
Sensitive DevTools bindings were either unguarded (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

  1. Load a non-local DevTools frontend A remote or otherwise non-devtools:// frontend is connected to DevToolsUIBindings. (inferred)
  2. Invoke an unguarded binding The frontend calls DispatchHttpRequest, which pre-fix ran with no locality check (devtools_ui_bindings.cc:1166-1192).
  3. 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.)
  4. 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

A frontend that should be treated as remote/untrusted could reach browser-process bindings that should be restricted to local DevTools — dispatching HTTP requests and using the Aida client — a missing-authorization issue in the browser process. Pre-fix, 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

FunctionChangeNotes
if
chrome/browser/devtools/devtools_ui_bindings.cc
modified

Files Changed

  • chrome/browser/devtools/devtools_ui_bindings.cc
  • chrome/browser/devtools/devtools_ui_bindings.h
  • chrome/browser/devtools/devtools_ui_bindings_unittest.cc

Audit Directions

  • Unguarded privileged bindings
    Enumerate every frontend-invokable DevToolsUIBindings method and confirm each sensitive one checks is_local_frontend_.
  • TOCTOU on trust state
    Replace live GetLastCommittedURL() / visible-URL trust checks with state captured at navigation commit to avoid races.
  • Fail-closed defaults
    Ensure trust flags like is_local_frontend_ default to false and are set only on a verified local commit.
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>();
Loading diff…

Regression Test / PoC

shipped with the fix
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>();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.