Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in DevTools
DescriptionIncorrect authorization in DevTools
ComponentDevTools
Bug ClassAuthorization 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.
Tracker529123409
Fix commitb781be4b5981 (chromium/src) +119/-2
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedAvadhut Mahamuni
Reported2026-06-29 (per vendor advisory)
Fix landed2026-09-02 (public git)
Disclosed2026-09-08
Patch gap6 days — fix public in source before the advisory

Background

DevTools UI bindings
the privileged DevToolsUIBindings object that a devtools:// frontend WebContents receives (tracked by its frontend_host_ member), granting the frontend host access to the DevTools embedder API.
`frontend_host_`
the member whose non-null state means active DevTools frontend bindings were installed for a WebContents; the patched code decides whether to install it.
Live opener vs. original opener chain
WebContents::GetOpener() returns the current live opener and becomes null once window.opener is severed (e.g. window.opener = null or rel="noopener"), whereas HasLiveOriginalOpenerChain() / GetFirstWebContentsInLiveOriginalOpenerChain() walk the original opener chain that persists even after severing.
Incorrect authorization
granting a privilege (here, DevTools frontend bindings) to a context that does not satisfy the intended precondition (a legitimate DevTools opener).

Root Cause Analysis

When a devtools:// navigation commits in a window that was opened by another window, the code (enclosing function DevToolsUIBindings::ReadyToCommitNavigation (inferred) — not named in the @@ hunk header) must verify that the opener is itself a valid DevTools frontend before installing frontend_host_. The pre-patch code consulted only the live immediate opener via content::WebContents* opener = web_contents_->GetOpener(); if that opener was null the entire validation block was skipped, and the navigation proceeded to install DevTools bindings as though it were a legitimate browser-initiated frontend. An attacker who opened the window and then severed the link (window.opener = null or rel="noopener") made GetOpener() return null, bypassing the !opener_bindings || !opener_bindings->frontend_host_ rejection check entirely.

The fix replaces the live-opener lookup with web_contents_->HasLiveOriginalOpenerChain() as the guard and web_contents_->GetFirstWebContentsInLiveOriginalOpenerChain() as the source, so the root of the original opener chain is validated even after window.opener is cleared. This works because the original opener chain records the true opener regardless of runtime severing, so the same !opener_bindings || !opener_bindings->frontend_host_ rejection now fires for the severed case, as the added OriginalOpenerWithoutDevToolsBindingsRejectedEvenIfOpenerSevered test confirms.

Key insight
The single mistake was validating the live opener (GetOpener()), which a page can null out, instead of the original opener chain; the fix authorizes DevTools bindings against GetFirstWebContentsInLiveOriginalOpenerChain() guarded by HasLiveOriginalOpenerChain(), closing the severed-opener bypass.

Attack Path

  1. Open a window from attacker content An attacker-controlled page opens a new window/tab whose navigation targets a devtools:// frontend URL such as devtools://devtools/bundled/devtools_app.html, so the opened WebContents has an opener that is not a DevTools frontend (inferred) — the exact web-facing trigger for reaching a devtools:// navigation is not shown in the diff.
  2. Sever the opener link Before the navigation commits, the relationship is broken via window.opener = null or rel="noopener", so web_contents_->GetOpener() returns null while the original opener chain still records the non-DevTools opener.
  3. Bypass the opener check In pre-patch ReadyToCommitNavigation (inferred), the if (content::RenderFrameHost* opener = web_contents_->GetOpener()) block is skipped because the live opener is null, so the !opener_bindings || !opener_bindings->frontend_host_ rejection never runs.
  4. Obtain DevTools bindings Because the validation was skipped, execution falls through and frontend_host_ is installed for a window whose opener never had DevTools frontend bindings, granting it the privileged DevTools UI bindings.
  5. Escalate via the DevTools embedder API (inferred) — With DevTools frontend bindings, the context can reach embedder capabilities exposed to legitimate frontends (e.g. DevTools protocol / local resource access); the concrete downstream capability and any RCE/sandbox impact are not demonstrated by this diff.

Impact Assessment

An attacker gains illegitimate DevTools frontend bindings (frontend_host_) for a window they opened, i.e. incorrect authorization in DevTools, by opening a window to a devtools:// frontend and severing window.opener before commit. The preconditions are the ability to open such a window and control over the opener relationship. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The advisory states only “Incorrect authorization in DevTools” and does not report exploitation in the wild or a severity rating in the provided metadata.

Changed Functions

FunctionChangeNotes
if
chrome/browser/devtools/devtools_ui_bindings.cc
modified
Delegate
chrome/browser/devtools/devtools_ui_bindings.h
modified
DevToolsUIBindingsNavigationTest
chrome/browser/devtools/devtools_ui_bindings_unittest.cc
modified
DevToolsUIBindingsSyncInfoTest
chrome/browser/devtools/devtools_ui_bindings_unittest.cc
modified

Files Changed

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

Audit Directions

  • Live-opener vs. original-opener checks
    Audit any authorization or origin decision keyed on GetOpener() / live window.opener; a page can null it out, so security-relevant opener checks should use HasLiveOriginalOpenerChain() / GetFirstWebContentsInLiveOriginalOpenerChain().
  • Skipped-branch fall-through on privilege grants
    Look for guards where a null/absent precondition causes the validation block to be skipped and execution to fall through to granting a privilege (installing bindings, enabling APIs) rather than to an explicit denial default.
  • `devtools://` and WebUI binding installation
    Review other paths that install privileged frontend/WebUI bindings for devtools:// or chrome:// WebContents to confirm they validate the true opener/initiator chain and cannot be reached by attacker-opened, opener-severed windows.
From b781be4b5981905b69c69643709baa64781a6bc8 Mon Sep 17 00:00:00 2001
From: Alex Rudenko <alexrudenko@chromium.org>
Date: Wed, 02 Sep 2026 03:10:37 -0700
Subject: [PATCH] Improve origin validation in devtools_ui_bindings

Fixed: 529123409
Change-Id: Ie16a65f099e66db08b5bd271f5af547e411b7cd1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8303113
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1690574}
---
diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc
index 6a47474d..157d0cff 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings.cc
@@ -3211,9 +3211,13 @@
     if (frontend_host_) {
       return;
     }
-    if (content::RenderFrameHost* opener = web_contents_->GetOpener()) {
+    // If the window was opened by another window, ensure the root opener in
+    // the live opener chain is a DevTools WebContents with active DevTools
+    // frontend bindings. This also covers cases where `window.opener` was
+    // severed (e.g. via `window.opener = null` or `rel="noopener"`).
+    if (web_contents_->HasLiveOriginalOpenerChain()) {
       content::WebContents* opener_wc =
-          content::WebContents::FromRenderFrameHost(opener);
+          web_contents_->GetFirstWebContentsInLiveOriginalOpenerChain();
       DevToolsUIBindings* opener_bindings =
           opener_wc ? DevToolsUIBindings::ForWebContents(opener_wc) : nullptr;
       if (!opener_bindings || !opener_bindings->frontend_host_) {
diff --git a/chrome/browser/devtools/devtools_ui_bindings.h b/chrome/browser/devtools/devtools_ui_bindings.h
index 260add27..0b6b6d3 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.h
+++ b/chrome/browser/devtools/devtools_ui_bindings.h
@@ -81,6 +81,7 @@
   friend class DevToolsUIBindingsDispatchHttpRequestTest;
   friend class DevToolsUIBindingsDispatchHttpRequestStreamingTest;
   friend class DevToolsUIBindingsLoadNetworkResourceTest;
+  friend class DevToolsUIBindingsNavigationTest;
 
  public:
   class Delegate {
@@ -168,6 +169,13 @@
                                        const std::string& script) {
     RegisterExtensionsAPI(origin, script);
   }
+  bool has_frontend_host_for_testing() const {
+    return frontend_host_ != nullptr;
+  }
+  void ReadyToCommitNavigationForTesting(
+      content::NavigationHandle* navigation_handle) {
+    ReadyToCommitNavigation(navigation_handle);
+  }
 
   void ShowDevToolsInfoBarForTesting(
       const std::u16string& message,
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index 716d446..2949666 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -26,9 +26,11 @@
 #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/mock_navigation_handle.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 "content/public/test/web_contents_tester.h"
 #include "google_apis/gaia/google_service_auth_error.h"
 #include "net/base/net_errors.h"
 #include "net/cookies/site_for_cookies.h"
@@ -393,6 +395,109 @@
   }
 }
 
+class DevToolsUIBindingsNavigationTest : public testing::Test {
+ public:
+  content::WebContents* CreateWebContents() {
+    return web_contents_factory_.CreateWebContents(&profile_);
+  }
+
+ protected:
+  content::BrowserTaskEnvironment task_environment_;
+  TestingProfile profile_;
+  content::TestWebContentsFactory web_contents_factory_;
+};
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       BrowserInitiatedNavigationCreatesFrontendHost) {
+  content::WebContents* web_contents = CreateWebContents();
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+  EXPECT_FALSE(bindings->has_frontend_host_for_testing());
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_TRUE(bindings->has_frontend_host_for_testing());
+}
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       OpenerWithoutDevToolsBindingsRejected) {
+  content::WebContents* opener_contents = CreateWebContents();
+  content::WebContents* web_contents = CreateWebContents();
+  content::WebContentsTester::For(web_contents)->SetOpener(opener_contents);
+  content::WebContentsTester::For(web_contents)
+      ->SetOriginalOpener(opener_contents);
+
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_FALSE(bindings->has_frontend_host_for_testing());
+}
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       OriginalOpenerWithoutDevToolsBindingsRejectedEvenIfOpenerSevered) {
+  content::WebContents* original_opener_contents = CreateWebContents();
+  content::WebContents* web_contents = CreateWebContents();
+  content::WebContentsTester::For(web_contents)
+      ->SetOriginalOpener(original_opener_contents);
+  // Ensure the live opener is null (simulating `window.opener = null`).
+  EXPECT_EQ(nullptr, web_contents->GetOpener());
+  EXPECT_TRUE(web_contents->HasLiveOriginalOpenerChain());
+
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_FALSE(bindings->has_frontend_host_for_testing());
+}
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       OpenerWithValidDevToolsBindingsAccepted) {
+  content::WebContents* opener_contents = CreateWebContents();
+  auto opener_bindings = std::make_unique<DevToolsUIBindings>(opener_contents);
+  content::MockNavigationHandle opener_handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      opener_contents->GetPrimaryMainFrame());
+  opener_handle.set_is_in_primary_main_frame(true);
+  opener_handle.set_is_renderer_initiated(false);
+  opener_bindings->ReadyToCommitNavigationForTesting(&opener_handle);
+  ASSERT_TRUE(opener_bindings->has_frontend_host_for_testing());
+
+  content::WebContents* web_contents = CreateWebContents();
+  content::WebContentsTester::For(web_contents)->SetOpener(opener_contents);
+  content::WebContentsTester::For(web_contents)
+      ->SetOriginalOpener(opener_contents);
+
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_TRUE(bindings->has_frontend_host_for_testing());
+}
+
 class DevToolsUIBindingsSyncInfoTest : public testing::Test {
  public:
   void SetUp() override {
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 716d446..2949666 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -26,9 +26,11 @@
 #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/mock_navigation_handle.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 "content/public/test/web_contents_tester.h"
 #include "google_apis/gaia/google_service_auth_error.h"
 #include "net/base/net_errors.h"
 #include "net/cookies/site_for_cookies.h"
@@ -393,6 +395,109 @@
   }
 }
 
+class DevToolsUIBindingsNavigationTest : public testing::Test {
+ public:
+  content::WebContents* CreateWebContents() {
+    return web_contents_factory_.CreateWebContents(&profile_);
+  }
+
+ protected:
+  content::BrowserTaskEnvironment task_environment_;
+  TestingProfile profile_;
+  content::TestWebContentsFactory web_contents_factory_;
+};
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       BrowserInitiatedNavigationCreatesFrontendHost) {
+  content::WebContents* web_contents = CreateWebContents();
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+  EXPECT_FALSE(bindings->has_frontend_host_for_testing());
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_TRUE(bindings->has_frontend_host_for_testing());
+}
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       OpenerWithoutDevToolsBindingsRejected) {
+  content::WebContents* opener_contents = CreateWebContents();
+  content::WebContents* web_contents = CreateWebContents();
+  content::WebContentsTester::For(web_contents)->SetOpener(opener_contents);
+  content::WebContentsTester::For(web_contents)
+      ->SetOriginalOpener(opener_contents);
+
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_FALSE(bindings->has_frontend_host_for_testing());
+}
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       OriginalOpenerWithoutDevToolsBindingsRejectedEvenIfOpenerSevered) {
+  content::WebContents* original_opener_contents = CreateWebContents();
+  content::WebContents* web_contents = CreateWebContents();
+  content::WebContentsTester::For(web_contents)
+      ->SetOriginalOpener(original_opener_contents);
+  // Ensure the live opener is null (simulating `window.opener = null`).
+  EXPECT_EQ(nullptr, web_contents->GetOpener());
+  EXPECT_TRUE(web_contents->HasLiveOriginalOpenerChain());
+
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_FALSE(bindings->has_frontend_host_for_testing());
+}
+
+TEST_F(DevToolsUIBindingsNavigationTest,
+       OpenerWithValidDevToolsBindingsAccepted) {
+  content::WebContents* opener_contents = CreateWebContents();
+  auto opener_bindings = std::make_unique<DevToolsUIBindings>(opener_contents);
+  content::MockNavigationHandle opener_handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      opener_contents->GetPrimaryMainFrame());
+  opener_handle.set_is_in_primary_main_frame(true);
+  opener_handle.set_is_renderer_initiated(false);
+  opener_bindings->ReadyToCommitNavigationForTesting(&opener_handle);
+  ASSERT_TRUE(opener_bindings->has_frontend_host_for_testing());
+
+  content::WebContents* web_contents = CreateWebContents();
+  content::WebContentsTester::For(web_contents)->SetOpener(opener_contents);
+  content::WebContentsTester::For(web_contents)
+      ->SetOriginalOpener(opener_contents);
+
+  auto bindings = std::make_unique<DevToolsUIBindings>(web_contents);
+
+  content::MockNavigationHandle handle(
+      GURL("devtools://devtools/bundled/devtools_app.html"),
+      web_contents->GetPrimaryMainFrame());
+  handle.set_is_in_primary_main_frame(true);
+  handle.set_is_renderer_initiated(false);
+
+  bindings->ReadyToCommitNavigationForTesting(&handle);
+
+  EXPECT_TRUE(bindings->has_frontend_host_for_testing());
+}
+
 class DevToolsUIBindingsSyncInfoTest : public testing::Test {
  public:
   void SetUp() override {
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.