Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in DevTools
DescriptionIncorrect authorization in DevTools
ComponentDevTools
Sub-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.
Tracker513791872
Fix commit63514758b188 (chromium/src) +101/-0
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-08-19 (public git)
Disclosed2026-09-22
Patch gap34 days — fix public in source before the advisory

Background

`scriptToEvaluateOnLoad`
an optional DevTools Page.reload parameter that injects and runs an attacker-supplied script in the freshly loaded document before its own scripts execute.
`ReadyToCommitNavigation`
the browser-process navigation lifecycle callback that fires once a response has been received and the destination origin is known, just before the document commits.
Server redirect
an HTTP 3xx response that sends the navigation to a different URL (potentially a different origin) than the one originally requested by Page.reload.
Last-committed origin
the security origin (url::Origin) of the document currently loaded in a frame, used as the trust boundary for deciding what may run in it.

Root Cause Analysis

When Page.reload was issued with scriptToEvaluateOnLoad, PageHandler stashed the script in pending_script_to_evaluate_on_load_ and delivered it unconditionally to whichever document ultimately committed the reload. Because a server redirect can steer that reload to an arbitrary cross-origin destination, the pending script was injected into a document whose origin differed from the page the operator (or extension) had targeted, violating the invariant that a script authorized for origin A must never execute in origin B.

The fix records the frame’s last-committed origin into initiating_origin_ at the moment the reload is requested, then in ReadyToCommitNavigation compares it against the navigation’s actual GetOriginToCommit() when a server redirect occurred and clears the pending script on any origin mismatch. This closes the gap because the origin-to-commit is only known after redirects are resolved, so the check happens exactly when the true destination origin is available and before the script is handed off for injection.

Key insight
The core mistake was binding the injected script to the requested reload rather than to the origin it was authorized for, so a redirect could silently retarget it; the fix re-validates the committing origin against the origin captured at request time and drops the script on cross-origin redirect.

Attack Path

  1. Obtain DevTools access A malicious or over-permissioned extension (or other Page domain client) gains the ability to issue Page.reload against a target tab. (inferred)
  2. Reload with an injected script The client calls Page.reload with scriptToEvaluateOnLoad while the tab is on a trusted origin the client is allowed to script, causing the script to be stored in pending_script_to_evaluate_on_load_.
  3. Redirect to a foreign origin The reloaded request returns a server redirect (3xx) whose Location points to a different origin, so the navigation commits into a document the client was never authorized to touch.
  4. Cross-origin script injection Because the pre-patch code delivered pending_script_to_evaluate_on_load_ regardless of the redirect, the attacker’s script executes in the cross-origin document, running with that origin’s privileges.
  5. Exfiltrate or manipulate victim origin With script running in the victim origin, the attacker can read page state, cookies exposed to script, or drive same-origin requests. (inferred)

Impact Assessment

An attacker gains cross-origin JavaScript execution inside a document rendered in the browser, letting authorized-for-one-origin script run in the context of a different origin reached via redirect, which is an authorization bypass in DevTools as the advisory states (“Incorrect authorization in DevTools”). The preconditions are a client able to issue Page.reload with scriptToEvaluateOnLoad (e.g., an extension with debugging privileges) and a reload target that server-redirects cross-origin. The advisory does not indicate exploitation in the wild, and any escalation beyond cross-origin script execution into broader data theft or further compromise is (inferred).

Changed Functions

FunctionChangeNotes
SkBitmap
content/browser/devtools/protocol/page_handler.h
modified

Files Changed

  • content/browser/devtools/protocol/page_handler.cc
  • content/browser/devtools/protocol/page_handler.h
  • third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js
  • third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt

Audit Directions

  • Deferred DevTools payloads across navigation boundaries
    Review any DevTools/content state stashed at request time and applied at commit (auto-attach scripts, header overrides, addScriptToEvaluateOnNewDocument) to confirm it is re-validated against the actual committing origin after redirects.
  • Redirect-changed origin assumptions
    Look for code that trusts the pre-navigation URL or origin without consulting GetOriginToCommit()/WasServerRedirect(), since server redirects can change the destination origin after authorization was decided.
  • Extension-reachable debugging surfaces
    Audit Page/Runtime/Debugger commands exposed to extensions for operations that grant one origin’s client the ability to influence another origin’s document.
From 63514758b188258126adc42aad239c47f895c2df Mon Sep 17 00:00:00 2001
From: Yulun Zeng <[email protected]>
Date: Wed, 19 Aug 2026 07:40:22 -0700
Subject: [PATCH] DevTools: Drop scriptToEvaluateOnLoad on cross-origin redirect

Remember the last-committed origin when Page.reload is issued and drop
the pending script in ReadyToCommitNavigation if it's redirecting to a
different origin. This disallows cross-origin script execution from an
extension.

TAG=agy
CONV=095e4c73-db36-48ea-80cd-a4b47ad1cc24

Bug: 513791872
Change-Id: I441690f1e340db3108d03b2ff1e6d1d551ec0dfc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8266043
Reviewed-by: Danil Somsikov <[email protected]>
Commit-Queue: Yulun Zeng <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1682289}
---
diff --git a/content/browser/devtools/protocol/page_handler.cc b/content/browser/devtools/protocol/page_handler.cc
index 431a0e3f..9fc740d 100644
--- a/content/browser/devtools/protocol/page_handler.cc
+++ b/content/browser/devtools/protocol/page_handler.cc
@@ -862,6 +862,7 @@
     have_pending_reload_ = true;
     pending_script_to_evaluate_on_load_ =
         script_to_evaluate_on_load.value_or("");
+    initiating_origin_ = outermost_main_frame->GetLastCommittedOrigin();
     navigation_controller.Reload(reload_type, false);
     callback->sendSuccess();
   } else {
@@ -2668,6 +2669,11 @@
     have_pending_reload_ = false;
     pending_script_to_evaluate_on_load_.clear();
   } else if (have_pending_reload_) {
+    if (navigation_request->WasServerRedirect() &&
+        !initiating_origin_.IsSameOriginWith(
+            navigation_request->GetOriginToCommit().value_or(url::Origin()))) {
+      pending_script_to_evaluate_on_load_.clear();
+    }
     prepare_for_reload_callback_.Run(
         std::move(pending_script_to_evaluate_on_load_));
     have_pending_reload_ = false;
diff --git a/content/browser/devtools/protocol/page_handler.h b/content/browser/devtools/protocol/page_handler.h
index 0ceca6f..b00b73d1 100644
--- a/content/browser/devtools/protocol/page_handler.h
+++ b/content/browser/devtools/protocol/page_handler.h
@@ -32,6 +32,7 @@
 #include "content/public/common/javascript_dialog_type.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom-forward.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 class SkBitmap;
 
@@ -308,6 +309,7 @@
   base::RepeatingCallback<void(std::string)> prepare_for_reload_callback_;
   bool have_pending_reload_ = false;
   std::string pending_script_to_evaluate_on_load_;
+  url::Origin initiating_origin_;
 
   Response AddScriptToEvaluateOnNewDocumentInternal(
       const std::string& source,
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt
new file mode 100644
index 0000000..2f7a70d1
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt
@@ -0,0 +1,11 @@
+Tests that Page.reload with scriptToEvaluateOnLoad drops the script on cross-origin redirects.
+
+--- Direct reload (no redirect) ---
+window.__injected: direct
+
+--- Same-origin redirect ---
+window.__injected: same-origin
+
+--- Cross-origin redirect ---
+window.__injected: undefined
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js
new file mode 100644
index 0000000..8977a98
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js
@@ -0,0 +1,82 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  const {session, dp} = await testRunner.startBlank(
+      `Tests that Page.reload with scriptToEvaluateOnLoad drops the script on cross-origin redirects.`);
+
+  const FetchHelper =
+      await testRunner.loadScript('../fetch/resources/fetch-test.js');
+  const helper = new FetchHelper(testRunner, testRunner.browserP());
+  helper.setEnableLogging(false);
+  await helper.enable();
+
+  await dp.Page.enable();
+
+  // 1. Direct reload (no redirect)
+  testRunner.log('\n--- Direct reload (no redirect) ---');
+  helper.onceRequest(/a.test\/direct/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Direct</body></html>'),
+  });
+  await dp.Page.navigate({url: 'http://a.test/direct'});
+  await dp.Page.onceLoadEventFired();
+
+  helper.onceRequest(/a.test\/direct/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Direct Reloaded</body></html>'),
+  });
+  dp.Page.reload({scriptToEvaluateOnLoad: 'window.__injected = "direct";'});
+  await dp.Page.onceLoadEventFired();
+  const directInjected = await session.evaluate('window.__injected');
+  testRunner.log(`window.__injected: ${directInjected}`);
+
+  // 2. Same-origin redirect
+  testRunner.log('\n--- Same-origin redirect ---');
+  helper.onceRequest(/a.test\/same-origin-start/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Same Origin Start</body></html>'),
+  });
+  await dp.Page.navigate({url: 'http://a.test/same-origin-start'});
+  await dp.Page.onceLoadEventFired();
+
+  helper.onceRequest(/a.test\/same-origin-start/).fulfill({
+    responseCode: 302,
+    responseHeaders: [
+      {name: 'Location', value: 'http://a.test/same-origin-dest'},
+    ],
+  });
+  helper.onceRequest(/a.test\/same-origin-dest/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Same Origin Dest</body></html>'),
+  });
+  dp.Page.reload(
+      {scriptToEvaluateOnLoad: 'window.__injected = "same-origin";'});
+  await dp.Page.onceLoadEventFired();
+  const sameOriginInjected = await session.evaluate('window.__injected');
+  testRunner.log(`window.__injected: ${sameOriginInjected}`);
+
+  // 3. Cross-origin redirect
+  testRunner.log('\n--- Cross-origin redirect ---');
+  helper.onceRequest(/a.test\/cross-origin-start/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Cross Origin Start</body></html>'),
+  });
+  await dp.Page.navigate({url: 'http://a.test/cross-origin-start'});
+  await dp.Page.onceLoadEventFired();
+
+  helper.onceRequest(/a.test\/cross-origin-start/).fulfill({
+    responseCode: 302,
+    responseHeaders: [
+      {name: 'Location', value: 'http://b.test/cross-origin-dest'},
+    ],
+  });
+  helper.onceRequest(/b.test\/cross-origin-dest/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Cross Origin Dest</body></html>'),
+  });
+  dp.Page.reload(
+      {scriptToEvaluateOnLoad: 'window.__injected = "cross-origin";'});
+  await dp.Page.onceLoadEventFired();
+  const crossOriginInjected = await session.evaluate('window.__injected');
+  testRunner.log(`window.__injected: ${crossOriginInjected}`);
+
+  testRunner.completeTest();
+})
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt
new file mode 100644
index 0000000..2f7a70d1
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script-expected.txt
@@ -0,0 +1,11 @@
+Tests that Page.reload with scriptToEvaluateOnLoad drops the script on cross-origin redirects.
+
+--- Direct reload (no redirect) ---
+window.__injected: direct
+
+--- Same-origin redirect ---
+window.__injected: same-origin
+
+--- Cross-origin redirect ---
+window.__injected: undefined
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js
new file mode 100644
index 0000000..8977a98
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/page/reload-redirect-script.js
@@ -0,0 +1,82 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  const {session, dp} = await testRunner.startBlank(
+      `Tests that Page.reload with scriptToEvaluateOnLoad drops the script on cross-origin redirects.`);
+
+  const FetchHelper =
+      await testRunner.loadScript('../fetch/resources/fetch-test.js');
+  const helper = new FetchHelper(testRunner, testRunner.browserP());
+  helper.setEnableLogging(false);
+  await helper.enable();
+
+  await dp.Page.enable();
+
+  // 1. Direct reload (no redirect)
+  testRunner.log('\n--- Direct reload (no redirect) ---');
+  helper.onceRequest(/a.test\/direct/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Direct</body></html>'),
+  });
+  await dp.Page.navigate({url: 'http://a.test/direct'});
+  await dp.Page.onceLoadEventFired();
+
+  helper.onceRequest(/a.test\/direct/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Direct Reloaded</body></html>'),
+  });
+  dp.Page.reload({scriptToEvaluateOnLoad: 'window.__injected = "direct";'});
+  await dp.Page.onceLoadEventFired();
+  const directInjected = await session.evaluate('window.__injected');
+  testRunner.log(`window.__injected: ${directInjected}`);
+
+  // 2. Same-origin redirect
+  testRunner.log('\n--- Same-origin redirect ---');
+  helper.onceRequest(/a.test\/same-origin-start/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Same Origin Start</body></html>'),
+  });
+  await dp.Page.navigate({url: 'http://a.test/same-origin-start'});
+  await dp.Page.onceLoadEventFired();
+
+  helper.onceRequest(/a.test\/same-origin-start/).fulfill({
+    responseCode: 302,
+    responseHeaders: [
+      {name: 'Location', value: 'http://a.test/same-origin-dest'},
+    ],
+  });
+  helper.onceRequest(/a.test\/same-origin-dest/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Same Origin Dest</body></html>'),
+  });
+  dp.Page.reload(
+      {scriptToEvaluateOnLoad: 'window.__injected = "same-origin";'});
+  await dp.Page.onceLoadEventFired();
+  const sameOriginInjected = await session.evaluate('window.__injected');
+  testRunner.log(`window.__injected: ${sameOriginInjected}`);
+
+  // 3. Cross-origin redirect
+  testRunner.log('\n--- Cross-origin redirect ---');
+  helper.onceRequest(/a.test\/cross-origin-start/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Cross Origin Start</body></html>'),
+  });
+  await dp.Page.navigate({url: 'http://a.test/cross-origin-start'});
+  await dp.Page.onceLoadEventFired();
+
+  helper.onceRequest(/a.test\/cross-origin-start/).fulfill({
+    responseCode: 302,
+    responseHeaders: [
+      {name: 'Location', value: 'http://b.test/cross-origin-dest'},
+    ],
+  });
+  helper.onceRequest(/b.test\/cross-origin-dest/).fulfill({
+    responseCode: 200,
+    body: btoa('<html><head></head><body>Cross Origin Dest</body></html>'),
+  });
+  dp.Page.reload(
+      {scriptToEvaluateOnLoad: 'window.__injected = "cross-origin";'});
+  await dp.Page.onceLoadEventFired();
+  const crossOriginInjected = await session.evaluate('window.__injected');
+  testRunner.log(`window.__injected: ${crossOriginInjected}`);
+
+  testRunner.completeTest();
+})
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.