CVE-2026-17890
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTcrdtp/cbor_test.cc |
modified |
Files Changed
crdtp/cbor.cccrdtp/cbor.hcrdtp/cbor_test.cc
Patch
From 888b4b7ee03b15e6381c36089d6748f79d9b35f8 Mon Sep 17 00:00:00 2001 From: Alex Rudenko <[email protected]> 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};
Regression Test / PoC
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};
Original Bug Report
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.cccontent/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.
- An attacker compromises a renderer process that has an active DevTools session connected (e.g., from an automated testing framework).
- The attacker crafts a malicious DevTools protocol message payload intended to spoof a response. This requires an
idfield. - 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 asSTRING16. - The compromised renderer sends this message to the browser via the Mojo
DispatchProtocolNotificationmethod. - In the browser,
DevToolsSession::ValidateMessagechecks for the presence of anidusingHasKeyInMap(..., "id")to ensure the renderer isn’t spoofing a response. - Because the key is encoded as
STRING16, theTokenTag() == CBORTokenTag::STRING8check inHasKeyInMapfails. The function returnsfalse, indicating noidwas found, and the browser accepts the message. - The browser forwards the message to the external DevTools client. If the client expects JSON, Chromium transcodes the CBOR, correctly decoding the
STRING16key into a standard JSON"id"string. If the client expects CBOR, standard parsers will also decode theSTRING16key correctly. - The external client parses the message, identifies the
idfield, and treats the attacker’s payload as a valid response to an earlier command.
Recommended Mitigation
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.