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
Tracker518043569
Fix commit59621765eb49 (chromium/src) +20/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-23

Files Changed

  • content/browser/devtools/devtools_session.cc
From 59621765eb49f2895805f256243ddb1f30297e55 Mon Sep 17 00:00:00 2001
From: Andrey Kosyakov <[email protected]>
Date: Mon, 15 Jun 2026 11:11:15 -0700
Subject: [PATCH] Copy devtools messages from renderer when backed by shmem

Fixed: 518043569
Change-Id: I9910fd3801de94f6e142d85d9e2c59ca098ddb49
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7935128
Commit-Queue: Andrey Kosyakov <[email protected]>
Reviewed-by: Peter Kvitek <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1646948}
---

diff --git a/content/browser/devtools/devtools_session.cc b/content/browser/devtools/devtools_session.cc
index fb29bca..846ae74e 100644
--- a/content/browser/devtools/devtools_session.cc
+++ b/content/browser/devtools/devtools_session.cc
@@ -573,8 +573,26 @@
     blink::mojom::DevToolsMessagePtr message,
     const std::string& session_id,
     const bool& is_notification) {
-  base::span<const uint8_t> message_span = message->data;
-  if (!ValidateMessage(session_id, /*expected_has_id=*/!is_notification,
+  // If BigBuffer is backed by shared memory, make a copy so that a compromised
+  // renderer wouldn't be able to mess with the message as we validate it.
+  std::vector<uint8_t> message_bytes;
+  base::span<const uint8_t> message_span;
+  switch (message->data.storage_type()) {
+    case mojo_base::BigBuffer::StorageType::kBytes:
+      message_span = message->data.byte_span();
+      break;
+    case mojo_base::BigBuffer::StorageType::kSharedMemory:
+      message_bytes.assign(message->data.begin(), message->data.end());
+      message_span = message_bytes;
+      break;
+    default:
+      // just keep span empty, this will cause renderer killed for invalid
+      // message below.
+      break;
+  }
+
+  if (message_span.empty() ||
+      !ValidateMessage(session_id, /*expected_has_id=*/!is_notification,
                        message_span)) {
     if (RenderProcessHost* process_host = agent_host->GetProcessHost()) {
       bad_message::ReceivedBadMessage(
Loading diff…

Original Bug Report

reported by [email protected]

BigBuffer TOCTOU in DevToolsSession allows compromised renderer to spoof CDP sessionId

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Time-of-Check Time-of-Use (TOCTOU) vulnerability exists in DevToolsSession::DispatchProtocolResponseOrNotification where incoming renderer-supplied messages are validated but not copied out of shared memory. A compromised renderer can pass a message backed by Mojo shared memory, satisfy the validation check, and then mutate the payload before the browser forwards it to the DevTools client. This could allow a compromised renderer to bypass validation checks and impersonate other DevTools sessions or the root session.

Affected files:

  • content/browser/devtools/devtools_session.cc

Estimated timestamp from git blame: 2026-05-04

Summary

A potential Time-of-Check Time-of-Use (TOCTOU) vulnerability exists in DevToolsSession::DispatchProtocolResponseOrNotification that could allow a compromised renderer to bypass security checks and spoof the sessionId of flattened-mode Chrome DevTools Protocol (CDP) messages. This potentially defeats the protections introduced in ValidateMessage.

Root Cause Analysis

The browser-side DevToolsSession implements blink::mojom::DevToolsSessionHost inside content/browser/devtools/devtools_session.cc. When a response or notification message is received from the renderer, the browser validates the message’s sessionId before forwarding it to the DevTools client:

void DevToolsSession::DispatchProtocolResponseOrNotification(
    DevToolsAgentHostClient* client,
    DevToolsAgentHostImpl* agent_host,
    blink::mojom::DevToolsMessagePtr message,
    const std::string& session_id,
    const bool& is_notification) {
  base::span<const uint8_t> message_span = message->data;
  if (!ValidateMessage(session_id, /*expected_has_id=*/!is_notification,
                       message_span)) {
    if (RenderProcessHost* process_host = agent_host->GetProcessHost()) {
      bad_message::ReceivedBadMessage(
          process_host, bad_message::RFH_INCONSISTENT_DEVTOOLS_MESSAGE);
    }
    return;
  }
  client->DispatchProtocolMessage(agent_host, message->data);
}

The issue is that message->data is a mojo_base::BigBuffer. When a BigBuffer is deserialized, it can be backed by a shared memory region (kSharedMemory). The implicit conversion of message->data to base::span<const uint8_t> yields message_span, which points directly to the mapped shared memory.

No copy is performed prior to or during the call to ValidateMessage. Because the memory is shared and remains writable by the sending process, a compromised renderer can maintain a writable mapping to this memory buffer and modify its contents after the validation check has passed but before the message is forwarded via client->DispatchProtocolMessage(agent_host, message->data).

As warned in the Mojo documentation (mojo/public/cpp/base/big_buffer.h): > SECURITY NOTE: When shmem is backing the message, it may be writable in the sending process while being read in the receiving process. If a BigBuffer is received from an untrustworthy process, you should make a copy of the data before processing it to avoid time-of-check time-of-use (TOCTOU) bugs.

Potential Attack Vector & Steps

Note: These are potential steps as we do not have a working proof of concept that has been successfully executed on running code.

An attacker with an already compromised renderer could potentially execute this race condition as follows:

  1. Ensure a flattened-mode DevTools client is attached to the tab (e.g., standard DevTools, Puppeteer, or an extension using debugger APIs).
  2. Create a Mojo shared buffer and write a valid JSON/CBOR message payload containing the renderer’s own legitimate sessionId.
  3. Send the message to the browser via DispatchProtocolNotification choosing the shared memory storage backing.
  4. Concurrently, a background thread in the compromised renderer continuously attempts to overwrite the sessionId field in the shared memory mapping (or remove it entirely to spoof the root session) immediately after the validation begins.
  5. In the browser, ValidateMessage parses the initial valid session ID and returns true. The browser then forwards the modified, post-TOCTOU message payload to the DevTools client.
  6. The DevTools client (e.g., standard frontend, Puppeteer, or debugger extension) processes the spoofed message as if it originated from the targeted session.

Security Impact

This is a potential bypass of the ValidateMessage mitigation. A compromised renderer with a DevTools session attached could spoof other DevTools sessions or impersonate the root session to perform privileged actions, resulting in a sandbox escape or origin bypass.

Suggested Fix

To secure the handler, copy the incoming message->data into a private, browser-owned memory buffer before performing any validation or processing:

void DevToolsSession::DispatchProtocolResponseOrNotification(
    DevToolsAgentHostClient* client,
    DevToolsAgentHostImpl* agent_host,
    blink::mojom::DevToolsMessagePtr message,
    const std::string& session_id,
    const bool& is_notification) {
  // Make a browser-side copy of the message data to prevent TOCTOU modifications
  std::vector<uint8_t> safe_copy(message->data.begin(), message->data.end());

  if (!ValidateMessage(session_id, /*expected_has_id=*/!is_notification,
                       safe_copy)) {
    if (RenderProcessHost* process_host = agent_host->GetProcessHost()) {
      bad_message::ReceivedBadMessage(
          process_host, bad_message::RFH_INCONSISTENT_DEVTOOLS_MESSAGE);
    }
    return;
  }
  client->DispatchProtocolMessage(agent_host, safe_copy);
}

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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