Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInjection in DevTools
DescriptionInjection in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker540021969
Fix commitfa24b81a15ef (chromium/src) +61/-21
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

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
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\": "
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