CVE-2026-10916
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/devtools/protocol/page_handler.cc |
modified |
Files Changed
content/browser/devtools/devtools_session.cccontent/browser/devtools/devtools_session.hcontent/browser/devtools/protocol/page_handler.cccontent/browser/devtools/protocol/page_handler.h
Patch
From cf0d8730a16218b74a20bf95ca3f778559537725 Mon Sep 17 00:00:00 2001 From: Andrey Kosyakov <[email protected]> Date: Wed, 06 May 2026 09:46:28 -0700 Subject: [PATCH] Maintain scripts to evaluate on new document on the browser side ... so that renderer-controlled session state cookie is not used for that. - move {Add,Remove}ScriptToEvaluateOn{Load,NewDocument} to the browser side; - store those in DevToolsBrowserAgentState, as opposed to render-managed session state where they used to be before; - explicitly propagate these to the renderer via mojo calls of newly-introduced DevtoolsSession methods, rather than falling through with CDP means, as the script ids need to be allocated on the browser side now; - Move InspectorInjectedScriptManager creation to DevToolsSession and manage scripts by DevToolsSession on the renderer side; - Move IsPausedForNewWindow() method from InspectorPageAgent::Client to DevtoolsAgent::Client; Bug: 497643690 Change-Id: Ic65790cd5f2e831309da26fe5787d6f029b35276 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7809308 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Andrey Kosyakov <[email protected]> Reviewed-by: Dmitry Gozman <[email protected]> Reviewed-by: Fred Shih <[email protected]> Cr-Commit-Position: refs/heads/main@{#1626253} --- diff --git a/content/browser/devtools/devtools_session.cc b/content/browser/devtools/devtools_session.cc index a9d7c7f..fdc9b0f 100644 --- a/content/browser/devtools/devtools_session.cc +++ b/content/browser/devtools/devtools_session.cc @@ -680,6 +680,26 @@ child_observers_.RemoveObserver(obs); } +void DevToolsSession::AddScriptToEvaluateOnNewDocument( + const std::string& identifier, + blink::mojom::ScriptToEvaluateOnNewDocumentPtr script, + bool run_immediately, + base::OnceClosure callback) { + if (session_.is_bound()) { + session_->AddScriptToEvaluateOnNewDocument( + identifier, std::move(script), run_immediately, std::move(callback)); + } else { + std::move(callback).Run(); + } +} + +void DevToolsSession::RemoveScriptToEvaluateOnNewDocument( + const std::string& identifier) { + if (session_.is_bound()) { + session_->RemoveScriptToEvaluateOnNewDocument(identifier); + } +} + void DevToolsSession::PrepareForReload(std::string script_to_evaluate_on_load) { script_to_evaluate_on_load_ = std::move(script_to_evaluate_on_load); io_session_->UnpauseAndTerminate(); diff --git a/content/browser/devtools/devtools_session.h b/content/browser/devtools/devtools_session.h index 1a43bf7..31eb8a5 100644 --- a/content/browser/devtools/devtools_session.h +++ b/content/browser/devtools/devtools_session.h @@ -140,6 +140,17 @@ friend class FlattenedDevToolsProtocolTest; + blink::mojom::BrowserOriginatingSessionState* browser_agent_state() { + return session_state_cookie_->browser_originating_session_state.get(); + } + + void AddScriptToEvaluateOnNewDocument( + const std::string& identifier, + blink::mojom::ScriptToEvaluateOnNewDocumentPtr script, + bool run_immediately, + base::OnceClosure callback); + void RemoveScriptToEvaluateOnNewDocument(const std::string& identifier); + base::RepeatingCallback<void(std::string)> MakePrepareForReloadCallback() { return base::BindRepeating(&DevToolsSession::PrepareForReload, base::Unretained(this)); diff --git a/content/browser/devtools/protocol/page_handler.cc b/content/browser/devtools/protocol/page_handler.cc index 5b5b5bd..40022fa 100644 --- a/content/browser/devtools/protocol/page_handler.cc +++ b/content/browser/devtools/protocol/page_handler.cc @@ -34,6 +34,7 @@ #include "content/browser/back_forward_cache/back_forward_cache_metrics.h" #include "content/browser/child_process_security_policy_impl.h" #include "content/browser/devtools/devtools_agent_host_impl.h" +#include "content/browser/devtools/devtools_session.h" #include "content/browser/devtools/protocol/browser_handler.h" #include "content/browser/devtools/protocol/devtools_mhtml_helper.h" #include "content/browser/devtools/protocol/emulation_handler.h" @@ -753,6 +754,95 @@ return Response::Success(); } +Response PageHandler::AddScriptToEvaluateOnNewDocumentInternal( + const std::string& source, + std::optional<std::string> world_name, + std::optional<bool> include_command_line_api, + std::optional<bool> run_immediately, + std::string* identifier, + base::OnceClosure callback) { + blink::mojom::BrowserOriginatingSessionState* state = + session()->browser_agent_state(); + + // Generate identifier. This currently uses an id that is 1 higher than the + // largest existent id, but is subject to change in the future. The clients + // should assume the id is an opaque string and should not presume anything + // about string content being a number or assume any other allocation logic. + int id = 1; + for (const auto& entry : state->scripts_to_evaluate_on_new_document) { + int entry_id = 0; + if (base::StringToInt(entry.first, &entry_id)) { + id = std::max(id, entry_id + 1); + } + } + *identifier = base::NumberToString(id); + + auto script = blink::mojom::ScriptToEvaluateOnNewDocument::New(); + script->source = source; + script->world_name = world_name.value_or(""); + script->include_command_line_api = include_command_line_api.value_or(false); + state->scripts_to_evaluate_on_new_document[*identifier] = script.Clone(); + + session()->AddScriptToEvaluateOnNewDocument(*identifier, std::move(script), + run_immediately.value_or(false), + std::move(callback)); + + return Response::Success(); +} + +Response PageHandler::RemoveScriptToEvaluateOnNewDocument( + const std::string& identifier) { + blink::mojom::BrowserOriginatingSessionState* state = + session()->browser_agent_state(); + + auto it = state->scripts_to_evaluate_on_new_document.find(identifier); + if (it == state->scripts_to_evaluate_on_new_document.end()) { + return Response::ServerError("Script not found"); + } + state->scripts_to_evaluate_on_new_document.erase(it); + + session()->RemoveScriptToEvaluateOnNewDocument(identifier); + + return Response::Success(); +} + +void PageHandler::AddScriptToEvaluateOnNewDocument( + const std::string& source, + std::optional<std::string> world_name, + std::optional<bool> include_command_line_api, + std::optional<bool> run_immediately, + std::unique_ptr<AddScriptToEvaluateOnNewDocumentCallback> callback) { + auto identifier = std::make_unique<std::string>(); + auto* identifier_ptr = identifier.get(); + + AddScriptToEvaluateOnNewDocumentInternal( + source, world_name, include_command_line_api, run_immediately, + identifier_ptr, + base::BindOnce( + [](std::unique_ptr<AddScriptToEvaluateOnNewDocumentCallback> cb, + std::unique_ptr<std::string> id) { cb->sendSuccess(*id); }, + std::move(callback), std::move(identifier))); +} + +void PageHandler::AddScriptToEvaluateOnLoad( + const std::string& source, + std::unique_ptr<AddScriptToEvaluateOnLoadCallback> callback) { + auto identifier = std::make_unique<std::string>(); + auto* identifier_ptr = identifier.get(); + + AddScriptToEvaluateOnNewDocumentInternal( + source, std::nullopt, std::nullopt, std::nullopt, identifier_ptr, + base::BindOnce( + [](std::unique_ptr<AddScriptToEvaluateOnLoadCallback> cb, + std::unique_ptr<std::string> id) { cb->sendSuccess(*id); }, + std::move(callback), std::move(identifier))); +} + +Response PageHandler::RemoveScriptToEvaluateOnLoad( + const std::string& identifier) { + return RemoveScriptToEvaluateOnNewDocument(identifier); +} + void PageHandler::Reload(std::optional<bool> bypassCache, std::optional<std::string> script_to_evaluate_on_load, std::optional<std::string> loader_id, diff --git a/content/browser/devtools/protocol/page_handler.h b/content/browser/devtools/protocol/page_handler.h index bdc6790..d31bf75 100644 --- a/content/browser/devtools/protocol/page_handler.h +++ b/content/browser/devtools/protocol/page_handler.h @@ -197,6 +197,20 @@ std::optional<bool> include_actionable_information, std::unique_ptr<GetAnnotatedPageContentCallback> callback) override; + void AddScriptToEvaluateOnNewDocument( + const std::string& source, + std::optional<std::string> world_name, + std::optional<bool> include_command_line_api, + std::optional<bool> run_immediately, + std::unique_ptr<AddScriptToEvaluateOnNewDocumentCallback> callback) + override;
Original Bug Report
UXSS and Site Isolation Bypass via Unvalidated DevToolsSessionState
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can inject malicious session state into an active DevTools session via the DevToolsSessionHost Mojo interface. This unvalidated state is forwarded by the browser to a new, cross-origin renderer upon navigation. The victim renderer then faithfully restores this state, potentially allowing the attacker to execute arbitrary JavaScript in the new origin’s context.
Affected files:
content/browser/devtools/devtools_session.ccthird_party/blink/renderer/core/inspector/inspector_page_agent.cccontent/browser/devtools/devtools_renderer_channel.ccthird_party/blink/renderer/core/inspector/inspector_session_state.h
Estimated timestamp from git blame: 2019-04-25
Description
There is a potential Site Isolation bypass and Universal Cross-Site Scripting (UXSS) vulnerability in Chrome’s DevTools architecture. The issue stems from the browser process blindly accepting and persisting DevTools session state updates from an untrusted renderer process, and then forwarding that poisoned state to a new, cross-origin renderer during navigation.
Technical Details
- State Poisoning via Mojo: In
content/browser/devtools/devtools_session.cc, theDevToolsSessionclass maintains asession_state_cookie_. When a renderer sends a state update via theDevToolsSessionHostMojo interface (e.g.,DispatchProtocolNotification), the browser callsApplySessionStateUpdates. This function iterates over theupdates->entriesand merges them directly intosession_state_cookie_without any validation or sanitization. - Cross-Origin Forwarding: When the tab navigates to a new origin, Site Isolation creates a new renderer process. To maintain the DevTools connection,
DevToolsRendererChannel::SetRendererInternalinvokesAttachToAgent. This calls theAttachDevToolsSessionMojo method on the new renderer’sDevToolsAgent, passing the poisonedsession_state_cookie_. - State Restoration and Execution: In the new victim renderer,
InspectorSessionStatedecodes the provided cookie. TheInspectorPageAgentbinds its state fields to deterministic keys based on initialization order:- Index 2 (
"Page.2/") maps to theenabled_boolean. - Index 7 (
"Page.7/") maps to thescripts_to_evaluate_on_load_string map.
- Index 2 (
By supplying a CBOR-encoded true for "Page.2/" and a malicious JavaScript payload for "Page.7/1", an attacker can force the victim renderer to enable the InspectorPageAgent and register a script to run on load. When InspectorPageAgent::DidCreateMainWorldContext fires for the new origin, it blindly executes the attacker’s injected script in the context of the victim’s main world.
Potential Reproduction Steps
(Note: Our tooling agent does not currently have the ability to run code, so these are suggested steps based on static analysis.)
- A user has a DevTools client (e.g., DevTools frontend, an extension using
chrome.debugger, or Puppeteer) attached to a tab visitinghttps://attacker.com. - The attacker achieves RCE to compromise the renderer process for
attacker.com. - Using the compromised renderer’s
DevToolsSessionHostMojo remote, the attacker callsDispatchProtocolNotificationwith a craftedupdatesmap containing:"Page.2/":[0xf5](CBOR encoding fortrue)"Page.7/1": CBOR-encoded string containing malicious JavaScript (e.g.,alert(document.domain)).
- The user or the attacker’s script navigates the tab to
https://victim.com. - The browser spins up a new renderer for
victim.com, passing the poisoned state cookie during DevTools attachment. - The injected JavaScript executes in the context of
victim.comonce the document commits and its main world context is created.
Suggested Fix
The browser process should not blindly trust DevToolsSessionState updates from the renderer, especially when those updates will be forwarded across process boundaries.
Potential remediations include:
- Browser-Side State Management: Sensitive DevTools state (like scripts to evaluate on load or agent enablement statuses) should be managed authoritatively in the browser process rather than round-tripping through the renderer’s
session_state_cookie_. - State Validation: Implement strict validation in
DevToolsSession::ApplySessionStateUpdatesto ensure that a compromised renderer cannot modify sensitive keys or inject arbitrary scripts. - Origin Scoping: Clear or heavily filter the
session_state_cookie_upon cross-process navigation to ensure origin-specific configurations do not leak or compromise a new renderer.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.