Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
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
Tracker524029061
Fix commit888b4b7ee03b (deps/inspector_protocol) +59/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST
crdtp/cbor_test.cc
modified

Files Changed

  • crdtp/cbor.cc
  • crdtp/cbor.h
  • crdtp/cbor_test.cc
From 888b4b7ee03b15e6381c36089d6748f79d9b35f8 Mon Sep 17 00:00:00 2001
From: Alex Rudenko <alexrudenko@chromium.org>
Date: Mon, 15 Jun 2026 07:34:56 +0000
Subject: [PATCH] Check String16 keys in HasKeyInMap

this method is used to check that CDP events do not have a sessionId
key. This CL fixes the bug that did not consider sessionId keys encoded
as utf-16.

Bug: 524029061
Change-Id: I3be7f2a020adaec0caca833895b896a947149ff6
---

diff --git a/crdtp/cbor.cc b/crdtp/cbor.cc
index 7e62d76..8d0ef70 100644
--- a/crdtp/cbor.cc
+++ b/crdtp/cbor.cc
@@ -1160,6 +1160,22 @@
           SpanEquals(tokenizer.GetString8(), key)) {
         return true;
       }
+      // We only support matching STRING16 keys if the search key is ASCII.
+      if (tokenizer.TokenTag() == CBORTokenTag::STRING16) {
+        span<uint8_t> rep = tokenizer.GetString16WireRep();
+        if (rep.size() == key.size() * 2) {
+          bool matches = true;
+          for (size_t ii = 0; ii < key.size(); ++ii) {
+            if (key[ii] > 127 || rep[ii * 2] != key[ii] ||
+                rep[ii * 2 + 1] != 0) {
+              matches = false;
+              break;
+            }
+          }
+          if (matches)
+            return true;
+        }
+      }
     }
     tokenizer.Next();
     is_key = !is_key;
diff --git a/crdtp/cbor.h b/crdtp/cbor.h
index d7bcfa2..f5ddd25 100644
--- a/crdtp/cbor.h
+++ b/crdtp/cbor.h
@@ -324,6 +324,7 @@
                                                   span<uint8_t> string8_key);
 // Safely checks if |key| exists in the top-level of a CBOR encoded map wrapped
 // in an envelope. Shallow parser that skips nested structures.
+// |key| should be ASCII. Supports STRING8 and STRING16 keys.
 // Returns true as soon as the key is found at the top level.
 CRDTP_EXPORT bool HasKeyInMap(span<uint8_t> message, span<uint8_t> key);
 
diff --git a/crdtp/cbor_test.cc b/crdtp/cbor_test.cc
index 4b23d36..e8b8ed4 100644
--- a/crdtp/cbor_test.cc
+++ b/crdtp/cbor_test.cc
@@ -1796,6 +1796,48 @@
   EXPECT_FALSE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key2")));
 }
 
+TEST(HasKeyInMapTest, FindsString16Key) {
+  std::vector<uint8_t> encoded;
+  EnvelopeEncoder envelope;
+  envelope.EncodeStart(&encoded);
+  encoded.push_back(EncodeIndefiniteLengthMapStart());
+
+  std::vector<uint16_t> key16 = {'k', 'e', 'y', '1'};
+  EncodeString16(SpanFrom(key16), &encoded);
+  EncodeString8(SpanFrom("value1"), &encoded);
+
+  EncodeString8(SpanFrom("key2"), &encoded);
+  EncodeInt32(42, &encoded);
+
+  encoded.push_back(EncodeStop());
+  envelope.EncodeStop(&encoded);
+
+  EXPECT_TRUE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key1")));
+  EXPECT_TRUE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key2")));
+}
+
+TEST(HasKeyInMapTest, DoesNotFindNonAsciiString16Key) {
+  std::vector<uint8_t> encoded;
+  EnvelopeEncoder envelope;
+  envelope.EncodeStart(&encoded);
+  encoded.push_back(EncodeIndefiniteLengthMapStart());
+
+  // "key_á" where á is U+00E1
+  std::vector<uint16_t> key16 = {'k', 'e', 'y', '_', 0x00e1};
+  EncodeString16(SpanFrom(key16), &encoded);
+  EncodeString8(SpanFrom("value1"), &encoded);
+
+  encoded.push_back(EncodeStop());
+  envelope.EncodeStop(&encoded);
+
+  // Searching with UTF-8 "key_á" should fail because it's non-ASCII.
+  // UTF-8 for á is \xc3\xa1
+  EXPECT_FALSE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key_\xc3\xa1")));
+
+  // Searching with ASCII "key_a" should also fail.
+  EXPECT_FALSE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key_a")));
+}
+
 TEST(HasKeyInMapTest, InvalidMessage) {
   std::vector<uint8_t> msg = {
       0xd8, 0x5a, 0, 0, 0, 2, EncodeIndefiniteLengthMapStart(), 42};
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/crdtp/cbor_test.cc b/crdtp/cbor_test.cc
index 4b23d36..e8b8ed4 100644
--- a/crdtp/cbor_test.cc
+++ b/crdtp/cbor_test.cc
@@ -1796,6 +1796,48 @@
   EXPECT_FALSE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key2")));
 }
 
+TEST(HasKeyInMapTest, FindsString16Key) {
+  std::vector<uint8_t> encoded;
+  EnvelopeEncoder envelope;
+  envelope.EncodeStart(&encoded);
+  encoded.push_back(EncodeIndefiniteLengthMapStart());
+
+  std::vector<uint16_t> key16 = {'k', 'e', 'y', '1'};
+  EncodeString16(SpanFrom(key16), &encoded);
+  EncodeString8(SpanFrom("value1"), &encoded);
+
+  EncodeString8(SpanFrom("key2"), &encoded);
+  EncodeInt32(42, &encoded);
+
+  encoded.push_back(EncodeStop());
+  envelope.EncodeStop(&encoded);
+
+  EXPECT_TRUE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key1")));
+  EXPECT_TRUE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key2")));
+}
+
+TEST(HasKeyInMapTest, DoesNotFindNonAsciiString16Key) {
+  std::vector<uint8_t> encoded;
+  EnvelopeEncoder envelope;
+  envelope.EncodeStart(&encoded);
+  encoded.push_back(EncodeIndefiniteLengthMapStart());
+
+  // "key_á" where á is U+00E1
+  std::vector<uint16_t> key16 = {'k', 'e', 'y', '_', 0x00e1};
+  EncodeString16(SpanFrom(key16), &encoded);
+  EncodeString8(SpanFrom("value1"), &encoded);
+
+  encoded.push_back(EncodeStop());
+  envelope.EncodeStop(&encoded);
+
+  // Searching with UTF-8 "key_á" should fail because it's non-ASCII.
+  // UTF-8 for á is \xc3\xa1
+  EXPECT_FALSE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key_\xc3\xa1")));
+
+  // Searching with ASCII "key_a" should also fail.
+  EXPECT_FALSE(HasKeyInMap(SpanFrom(encoded), SpanFrom("key_a")));
+}
+
 TEST(HasKeyInMapTest, InvalidMessage) {
   std::vector<uint8_t> msg = {
       0xd8, 0x5a, 0, 0, 0, 2, EncodeIndefiniteLengthMapStart(), 42};
Loading diff…

Original Bug Report

reported by rj...@google.com

Bypass of HasKeyInMap via STRING16 keys in CBOR leading to DevTools response spoofing

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 logic flaw in the HasKeyInMap function within the CRDTP library ignores map keys encoded as STRING16. This allows a compromised renderer process to bypass security checks in DevToolsSession::ValidateMessage by encoding forbidden fields (like ‘id’) as STRING16. Consequently, a compromised renderer can spoof DevTools responses to external clients.

Affected files:

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

Estimated timestamp from git blame: 2026-05-18

Summary

A vulnerability in the HasKeyInMap function within Chromium’s CRDTP (Chrome DevTools Protocol) library allows for a bypass of security validations on CBOR-encoded messages. By encoding map keys using the STRING16 major type instead of the standard STRING8, a compromised renderer can evade checks performed by the browser process. This can be exploited to spoof DevTools responses, enabling cross-session attacks in environments using the CBOR binary protocol (e.g., automated testing with Puppeteer or WebDriver BiDi) or JSON clients where Chromium transcodes the CBOR payload.

Technical Details

The HasKeyInMap function in third_party/inspector_protocol/crdtp/cbor.cc is used by the browser to verify the structure of CBOR messages from the renderer without full deserialization. A critical use case is in DevToolsSession::ValidateMessage (in content/browser/devtools/devtools_session.cc), where it ensures that notification messages do not contain an id field. The presence of an id field would cause the message to be treated as a response by the recipient.

However, the implementation of HasKeyInMap incorrectly restricts its search for map keys to the STRING8 encoding:

// third_party/inspector_protocol/crdtp/cbor.cc
if (is_key) {
  if (tokenizer.TokenTag() == CBORTokenTag::STRING8 &&
      SpanEquals(tokenizer.GetString8(), key)) {
    return true;
  }
}

The CRDTP tokenizer identifies both STRING8 (UTF-8) and STRING16 (UTF-16, via CBOR Major Type 2 BYTE_STRING) tokens. HasKeyInMap ignores STRING16 tokens while still advancing the tokenizer. This allows a compromised renderer to insert an id key encoded as STRING16. Because HasKeyInMap skips these tokens without error, it returns false (key not found) when searching for “id”.

Potential Exploitation Steps

Note: These are suggested steps based on static analysis.

  1. An attacker compromises a renderer process that has an active DevTools session connected (e.g., from an automated testing framework).
  2. The attacker crafts a malicious DevTools protocol message payload intended to spoof a response. This requires an id field.
  3. The attacker encodes the payload in CBOR. Critically, instead of encoding the map key "id" as a standard UTF-8 string, the attacker encodes it as UTF-16 bytes using the CBOR byte string format (Major Type 2), which the CRDTP tokenizer parses as STRING16.
  4. The compromised renderer sends this message to the browser via the Mojo DispatchProtocolNotification method.
  5. In the browser, DevToolsSession::ValidateMessage checks for the presence of an id using HasKeyInMap(..., "id") to ensure the renderer isn’t spoofing a response.
  6. Because the key is encoded as STRING16, the TokenTag() == CBORTokenTag::STRING8 check in HasKeyInMap fails. The function returns false, indicating no id was found, and the browser accepts the message.
  7. The browser forwards the message to the external DevTools client. If the client expects JSON, Chromium transcodes the CBOR, correctly decoding the STRING16 key into a standard JSON "id" string. If the client expects CBOR, standard parsers will also decode the STRING16 key correctly.
  8. The external client parses the message, identifies the id field, and treats the attacker’s payload as a valid response to an earlier command.

HasKeyInMap should be updated to check for STRING16 keys. Since common protocol keys like “id” are ASCII, the check can compare the UTF-16 wire representation against the search key, or reject the message if a map key is encountered that is not STRING8 (as top-level CRDTP properties are expected to be ASCII/UTF-8).

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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