High watch CVSS 8.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInjection in DevTools
DescriptionInjection in DevTools
ComponentDevTools
Bug ClassLogic 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.
Tracker540021969
Fix commitfa24b81a15ef (chromium/src) +61/-21
CWECWE-74
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

CDP (Chrome DevTools Protocol)
the message protocol DevTools uses to control and observe a page, carried between the (untrusted) renderer’s DevTools agent and the (trusted) browser process, then forwarded to the DevTools client/front-end.
CBOR vs JSON encoding
CDP messages travel either as binary CBOR or as text JSON, and crdtp::cbor::IsCBORMessage distinguishes the two by inspecting the byte stream.
`DevTools​Session::UsesBinaryProtocol()`
a property of the attached client that states whether that client speaks binary (CBOR) or text (JSON), i.e. which encoding the browser actually requested from the renderer.
`DevToolsSession::ValidateMessage`
the browser-side gate that a renderer-originated CDP message must pass (session-id match, id expectations) before the browser forwards it onward.

Root Cause Analysis

Renderer-originated CDP messages arrive at the browser process and are checked by DevToolsSession::ValidateMessage before being forwarded to the attached DevTools client. Before the patch, ValidateMessage accepted a message in either encoding regardless of which encoding the browser had negotiated: if the bytes were CBOR it validated them as CBOR, and otherwise it fell through to crdtp::json::ConvertJSONToCBOR and treated them as JSON. This violated the invariant that a well-behaved renderer sends exactly the encoding the browser asked for (UsesBinaryProtocol()), so a compromised renderer could deliberately send the other encoding and have it accepted.

The fix threads the negotiated encoding into the check as the new expect_cbor parameter and adds if (expect_cbor != is_cbor) return false;, rejecting any message whose actual encoding does not match the requested one and terminating the offending renderer via bad_message::ReceivedBadMessage. This closes the format-confusion gap so the encoding of forwarded messages is now guaranteed to match what the client parser expects.

Key insight
The single mistake was that ValidateMessage inferred the message encoding from the bytes themselves instead of enforcing the encoding the browser had negotiated, letting a renderer choose an unexpected format; the fix makes the browser compare the observed encoding against client->UsesBinaryProtocol() and reject any mismatch.

Attack Path

  1. Attach a DevTools session A compromised or malicious renderer participates in a CDP session whose client has negotiated a specific wire encoding (JSON or CBOR) via UsesBinaryProtocol().
  2. Send a mismatched-encoding message The renderer emits a CDP message in the encoding the browser did not request (CBOR when JSON was expected, or vice versa), which pre-patch ValidateMessage still accepted because it only auto-detected the format rather than enforcing it.
  3. Bypass encoding-dependent validation and forward The message passes the browser-side gate and is forwarded toward the DevTools client, whose parser expects the negotiated encoding, creating a format-confusion condition in the trusted DevTools channel.
  4. Injection into DevTools The mismatched message is interpreted by the front-end/client differently than the browser validated it, allowing the renderer to inject content into the DevTools protocol stream (inferred beyond the format-confusion the patch directly demonstrates).

Impact Assessment

A compromised renderer could smuggle a CDP message in an encoding other than the one the browser negotiated, defeating the browser-side format assumption and enabling injection into the trusted DevTools protocol channel that the front-end consumes. The precondition is an active DevTools/CDP session and a renderer able to send crafted agent messages; the classified impact is “Injection in DevTools” (a LogicError). Any escalation beyond format confusion and protocol injection — such as executing attacker-controlled DevTools commands or gaining code execution in the DevTools/browser context — is (inferred) and not established by the patch itself.

Changed Functions

FunctionChangeNotes
if
content/browser/devtools/devtools_session.cc
modified
TEST
content/browser/devtools/devtools_session_unittest.cc
modified

Files Changed

  • content/browser/devtools/devtools_session.cc
  • content/browser/devtools/devtools_session.h
  • content/browser/devtools/devtools_session_unittest.cc

Audit Directions

  • Format/encoding assumptions on IPC boundaries
    Audit other renderer-to-browser channels that auto-detect a payload’s encoding (CBOR/JSON, protobuf/text, etc.) instead of enforcing the negotiated format, since detection-based handling invites format confusion.
  • Validation that omits negotiated context
    Look for validators like ValidateMessage that judge a message purely on its content while ignoring session/connection state (expected encoding, client type, protocol mode) the caller already knows.
  • DevTools/CDP message forwarding
    Review paths in content/browser/devtools/ where renderer-originated messages are validated in the browser then reinterpreted by the front-end, ensuring the encoding and structure validated match exactly what the downstream consumer parses.
From fa24b81a15efa7af73e4c8a7b3ea8446db3b57b7 Mon Sep 17 00:00:00 2001
From: Andrey Kosyakov <caseq@chromium.org>
Date: Thu, 30 Jul 2026 14:17:41 -0700
Subject: [PATCH] Ensure renderer sends CDP messages in expected format

Validate that CBOR is coming from the renderer if and only if we requested it.

Fixed: 540021969
Change-Id: Ie4eb1550ccf0de9510beb04a0bb72cb4fe64cfee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8176419
Reviewed-by: Peter Kvitek <kvitekp@chromium.org>
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671419}
---
diff --git a/content/browser/devtools/devtools_session.cc b/content/browser/devtools/devtools_session.cc
index b3f7a67c..6c555ad 100644
--- a/content/browser/devtools/devtools_session.cc
+++ b/content/browser/devtools/devtools_session.cc
@@ -591,9 +591,12 @@
       break;
   }
 
-  if (message_span.empty() ||
-      !ValidateMessage(session_id, /*expected_has_id=*/!is_notification,
-                       message_span)) {
+  const bool message_is_valid =
+      !message_span.empty() &&
+      ValidateMessage(session_id,
+                      /*expected_has_id=*/!is_notification, message_span,
+                      /*expect_cbor */ client->UsesBinaryProtocol());
+  if (!message_is_valid) {
     if (RenderProcessHost* process_host = agent_host->GetProcessHost()) {
       bad_message::ReceivedBadMessage(
           process_host, bad_message::RFH_INCONSISTENT_DEVTOOLS_MESSAGE);
@@ -771,11 +774,18 @@
 // static
 bool DevToolsSession::ValidateMessage(const std::string& expected_session_id,
                                       const bool expected_has_id,
-                                      base::span<const uint8_t> message) {
+                                      base::span<const uint8_t> message,
+                                      bool expect_cbor) {
   std::vector<uint8_t> cbor_message;
   crdtp::span<uint8_t> span_message = crdtp::SpanFrom(message);
 
-  if (!crdtp::cbor::IsCBORMessage(span_message)) {
+  const bool is_cbor = crdtp::cbor::IsCBORMessage(span_message);
+  if (expect_cbor != is_cbor) {
+    // The renderer has sent a message in the format different that we asked
+    // for, something is fishy.
+    return false;
+  }
+  if (!is_cbor) {
     if (!crdtp::json::ConvertJSONToCBOR(span_message, &cbor_message).ok()) {
       return false;  // Safely terminate renderer on malformed JSON
     }
diff --git a/content/browser/devtools/devtools_session.h b/content/browser/devtools/devtools_session.h
index e1d63e7..0e95a1d 100644
--- a/content/browser/devtools/devtools_session.h
+++ b/content/browser/devtools/devtools_session.h
@@ -65,7 +65,8 @@
   static CONTENT_EXPORT bool ValidateMessage(
       const std::string& expected_session_id,
       const bool expected_has_id,
-      base::span<const uint8_t> message);
+      base::span<const uint8_t> message,
+      bool expect_cbor);
 
   // For sessions attached to the Tab target, the mode is set to TabTarget.
   // For other sessions, the mode is inherited from the parent.
diff --git a/content/browser/devtools/devtools_session_unittest.cc b/content/browser/devtools/devtools_session_unittest.cc
index 59c6465..bb27382a 100644
--- a/content/browser/devtools/devtools_session_unittest.cc
+++ b/content/browser/devtools/devtools_session_unittest.cc
@@ -30,78 +30,90 @@
 
 TEST(DevToolsSessionValidateMessageTest, MalformedJson) {
   std::vector<uint8_t> message = ToVector("{invalid cbor/json");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, EmptyMessage) {
   std::vector<uint8_t> message;
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ValidJson_RootSession_NoIdExpected_Valid) {
   std::vector<uint8_t> message =
       ToVector("{\"method\": \"Page.loadEventFired\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ValidCbor_RootSession_NoIdExpected_Valid) {
   std::vector<uint8_t> message =
       JsonToCbor("{\"method\": \"Page.loadEventFired\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message,
+                                               /*expect_cbor=*/true));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      RootSession_NoIdExpected_HasId_Invalid) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"method\": \"Page.loadEventFired\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_HasId_Valid) {
   std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_NoId_Valid) {
   // ValidateMessage does not enforce 'id' presence when expected_has_id is
   // true.
   std::vector<uint8_t> message = ToVector("{\"result\": {}}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, RootSession_HasSessionId_Invalid) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"sessionId\": \"123\", \"result\": {}}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_ValidMatchingSessionId_NoIdExpected) {
   std::vector<uint8_t> message = ToVector(
       "{\"method\": \"Page.loadEventFired\", \"sessionId\": \"session1\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", false, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", false, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_ValidMatchingSessionId_IdExpected) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", true, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_MissingSessionId_Invalid) {
   std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_MismatchingSessionId_Invalid) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session2\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
@@ -109,7 +121,8 @@
   std::vector<uint8_t> message = ToVector(
       "{\"id\": 1, \"method\": \"Page.loadEventFired\", \"sessionId\": "
       "\"session1\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
@@ -117,7 +130,8 @@
   std::vector<uint8_t> message = ToVector(
       "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
       "\"session1\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
@@ -125,7 +139,22 @@
   std::vector<uint8_t> message = ToVector(
       "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
       "\"session2\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
+}
+
+TEST(DevToolsSessionValidateMessageTest, CborMessage_ExpectCborFalse_Invalid) {
+  std::vector<uint8_t> message =
+      JsonToCbor("{\"method\": \"Page.loadEventFired\"}");
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
+}
+
+TEST(DevToolsSessionValidateMessageTest, JsonMessage_ExpectCborTrue_Invalid) {
+  std::vector<uint8_t> message =
+      ToVector("{\"method\": \"Page.loadEventFired\"}");
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/true));
 }
 
 }  // namespace
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/devtools/devtools_session_unittest.cc b/content/browser/devtools/devtools_session_unittest.cc
index 59c6465..bb27382a 100644
--- a/content/browser/devtools/devtools_session_unittest.cc
+++ b/content/browser/devtools/devtools_session_unittest.cc
@@ -30,78 +30,90 @@
 
 TEST(DevToolsSessionValidateMessageTest, MalformedJson) {
   std::vector<uint8_t> message = ToVector("{invalid cbor/json");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, EmptyMessage) {
   std::vector<uint8_t> message;
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ValidJson_RootSession_NoIdExpected_Valid) {
   std::vector<uint8_t> message =
       ToVector("{\"method\": \"Page.loadEventFired\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ValidCbor_RootSession_NoIdExpected_Valid) {
   std::vector<uint8_t> message =
       JsonToCbor("{\"method\": \"Page.loadEventFired\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message,
+                                               /*expect_cbor=*/true));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      RootSession_NoIdExpected_HasId_Invalid) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"method\": \"Page.loadEventFired\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_HasId_Valid) {
   std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_NoId_Valid) {
   // ValidateMessage does not enforce 'id' presence when expected_has_id is
   // true.
   std::vector<uint8_t> message = ToVector("{\"result\": {}}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest, RootSession_HasSessionId_Invalid) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"sessionId\": \"123\", \"result\": {}}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_ValidMatchingSessionId_NoIdExpected) {
   std::vector<uint8_t> message = ToVector(
       "{\"method\": \"Page.loadEventFired\", \"sessionId\": \"session1\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", false, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", false, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_ValidMatchingSessionId_IdExpected) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\"}");
-  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", true, message,
+                                               /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_MissingSessionId_Invalid) {
   std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
      ChildSession_MismatchingSessionId_Invalid) {
   std::vector<uint8_t> message =
       ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session2\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
@@ -109,7 +121,8 @@
   std::vector<uint8_t> message = ToVector(
       "{\"id\": 1, \"method\": \"Page.loadEventFired\", \"sessionId\": "
       "\"session1\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", false, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", false, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
@@ -117,7 +130,8 @@
   std::vector<uint8_t> message = ToVector(
       "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
       "\"session1\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
 }
 
 TEST(DevToolsSessionValidateMessageTest,
@@ -125,7 +139,22 @@
   std::vector<uint8_t> message = ToVector(
       "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
       "\"session2\"}");
-  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message,
+                                                /*expect_cbor=*/false));
+}
+
+TEST(DevToolsSessionValidateMessageTest, CborMessage_ExpectCborFalse_Invalid) {
+  std::vector<uint8_t> message =
+      JsonToCbor("{\"method\": \"Page.loadEventFired\"}");
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/false));
+}
+
+TEST(DevToolsSessionValidateMessageTest, JsonMessage_ExpectCborTrue_Invalid) {
+  std::vector<uint8_t> message =
+      ToVector("{\"method\": \"Page.loadEventFired\"}");
+  EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message,
+                                                /*expect_cbor=*/true));
 }
 
 }  // namespace
Loading diff…

Original Bug Report

reported by vm...@google.com

DevToolsSession ValidateMessage bypass via embedded-NUL in CBOR STRING8 payload

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 validation bypass in DevToolsSession::ValidateMessage allows a compromised renderer to inject arbitrary Chrome DevTools Protocol (CDP) commands into the root browser session. By placing a NUL-delimited JSON string inside a CBOR STRING8 value, the outer CBOR envelope size check is satisfied. When written verbatim to an external ASCIIZ pipe, the client splits the stream on the embedded NULs and parses the injected frame as a standalone root-level command.

Affected files:

  • content/browser/devtools/devtools_session.cc
  • third_party/inspector_protocol/crdtp/cbor.cc
  • content/browser/devtools/devtools_pipe_handler.cc
  • content/browser/devtools/protocol/target_handler.cc

Estimated timestamp from git blame: 2026-06-26

Root Cause Analysis

In content/browser/devtools/devtools_session.cc, the security check DevToolsSession::ValidateMessage ensures that a child session message from a renderer does not contain trailing bytes (which previously allowed pipe-frame injection) and verifies top-level parameters such as sessionId and the absence of an id field for notifications:

// content/browser/devtools/devtools_session.cc:783-794
} else {
  // Perform top-level validation of CBOR envelope.
  crdtp::StatusOr<size_t> status_or_outer_size =
      crdtp::cbor::CheckCBORMessage(span_message);
  if (!status_or_outer_size.ok()) { ... return false; }
  if (status_or_outer_size.value() != message.size()) {
    DLOG(ERROR) << "Unexpected trailing data in CBOR message from child";
    return false;
  }
}

While this check correctly rejects trailing data after the first CBOR envelope, it does not validate the contents of fields inside the envelope. When a compromised renderer sends CBOR, crdtp::cbor::CheckCBORMessage only verifies the envelope header and the indefinite-length map start. Top-level keys and values are then parsed with the CBORTokenizer (third_party/inspector_protocol/crdtp/cbor.cc).

When encountering a MajorType::STRING (STRING8) token, the tokenizer reads the length and skips the bytes as an opaque payload without content validation or checking for embedded NUL (0x00) characters:

// third_party/inspector_protocol/crdtp/cbor.cc:759-771
case MajorType::STRING: {  // STRING8.
  if (!bytes_read || token_start_internal_value_ > kMaxValidLength) {
    SetError(Error::CBOR_INVALID_STRING8); return;
  }
  uint64_t token_byte_length = token_start_internal_value_ + bytes_read;
  if (token_byte_length > remaining_bytes) {
    SetError(Error::CBOR_INVALID_STRING8); return;
  }
  SetToken(CBORTokenTag::STRING8, static_cast<size_t>(token_byte_length));
  return;
}

Because the raw unchanged buffer is forwarded to the client (client->DispatchProtocolMessage(agent_host, message->data)) on the renderer-originating path without format conversion, the malicious CBOR is ultimately written verbatim onto the ASCIIZ debugging pipe:

// content/browser/devtools/devtools_pipe_handler.cc:294-297
void PipeWriterASCIIZ::WriteIntoPipe(std::string message) override {
  WriteBytes(message.data(), message.size());
  WriteBytes("\0", 1);
}

Potential Attack Scenario

  1. A compromised renderer for a child session S constructs a custom CBOR map satisfying outer_size == message.size().
  2. The map contains "sessionId": S, a dummy parameter key "z", and a corresponding value encoded as a CBOR STRING8 containing: <padding> \0 <INJECTED_JSON> \0 <padding>, where <INJECTED_JSON> is a root-session CDP command (e.g. {"method":"Browser.getVersion","id":1000}).
  3. The renderer transmits this via the Mojo interface blink.mojom.DevToolsSessionHost.DispatchProtocolNotification.
  4. DevToolsSession::ValidateMessage passes because the outer envelope size matches and no top-level id exists. The nested STRING8 payload is skipped as a single token without inspection.
  5. The raw CBOR buffer is written verbatim to the external ASCIIZ pipe via PipeWriterASCIIZ.
  6. The external client (e.g., Puppeteer or Playwright running with pipe:true) reads the stream and splits it on NUL (0x00) bytes.
  7. The first segment (CBOR header) and third segment (CBOR trailer) fail JSON parsing and are discarded, but the middle segment (<INJECTED_JSON>) is parsed successfully as a root browser-level command, bypassing sandbox boundaries.

Note: These steps are based on static code analysis; our automated tools do not currently have the capability to execute code or run dynamic proofs of concept.

Suggested Fix

On the renderer-to-client message routing path, Chromium should enforce that messages are serialized to the expected protocol format of the destination client. Specifically, in DevToolsSession::DispatchProtocolResponseOrNotification, if the client is not using a binary protocol (!client->UsesBinaryProtocol()), the CBOR message should be translated to JSON before being sent to the client, which will automatically escape any control characters and NUL bytes inside string values.

Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908


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