Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in DevTools
DescriptionInsufficient validation of untrusted input in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker497643690
Fix commitcf0d8730a162 (chromium/src) +266/-178
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
for
content/browser/devtools/protocol/page_handler.cc
modified

Files Changed

  • content/browser/devtools/devtools_session.cc
  • content/browser/devtools/devtools_session.h
  • content/browser/devtools/protocol/page_handler.cc
  • content/browser/devtools/protocol/page_handler.h
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;
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/core/inspector/inspector_page_agent.cc
  • content/browser/devtools/devtools_renderer_channel.cc
  • third_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

  1. State Poisoning via Mojo: In content/browser/devtools/devtools_session.cc, the DevToolsSession class maintains a session_state_cookie_. When a renderer sends a state update via the DevToolsSessionHost Mojo interface (e.g., DispatchProtocolNotification), the browser calls ApplySessionStateUpdates. This function iterates over the updates->entries and merges them directly into session_state_cookie_ without any validation or sanitization.
  2. Cross-Origin Forwarding: When the tab navigates to a new origin, Site Isolation creates a new renderer process. To maintain the DevTools connection, DevToolsRendererChannel::SetRendererInternal invokes AttachToAgent. This calls the AttachDevToolsSession Mojo method on the new renderer’s DevToolsAgent, passing the poisoned session_state_cookie_.
  3. State Restoration and Execution: In the new victim renderer, InspectorSessionState decodes the provided cookie. The InspectorPageAgent binds its state fields to deterministic keys based on initialization order:
    • Index 2 ("Page.2/") maps to the enabled_ boolean.
    • Index 7 ("Page.7/") maps to the scripts_to_evaluate_on_load_ string map.

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.)

  1. A user has a DevTools client (e.g., DevTools frontend, an extension using chrome.debugger, or Puppeteer) attached to a tab visiting https://attacker.com.
  2. The attacker achieves RCE to compromise the renderer process for attacker.com.
  3. Using the compromised renderer’s DevToolsSessionHost Mojo remote, the attacker calls DispatchProtocolNotification with a crafted updates map containing:
    • "Page.2/": [0xf5] (CBOR encoding for true)
    • "Page.7/1": CBOR-encoded string containing malicious JavaScript (e.g., alert(document.domain)).
  4. The user or the attacker’s script navigates the tab to https://victim.com.
  5. The browser spins up a new renderer for victim.com, passing the poisoned state cookie during DevTools attachment.
  6. The injected JavaScript executes in the context of victim.com once 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::ApplySessionStateUpdates to 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.

View on issue tracker