CVE-2026-95274
Overview
Background
- `crdtp`
- Chrome’s DevTools-protocol support library, which includes a hand-written JSON parser in
crdtp/json.ccused to read CDP messages rather than deferring to V8’sJSON.parse(). - JSON comments
- non-standard
//line and/* ... */block sequences that RFC 8259 JSON forbids and thatJSON.parse()rejects. - Parser differential
- a logic-bug class in which two parsers consuming the same bytes disagree on the result, so a message can mean one thing to one consumer and something else to another.
- `SkipComment` / `SkipWhitespaceAndComments`
- the removed helpers that let the CDP parser silently consume comment runs while scanning for the next token.
Root Cause Analysis
Before the patch, the crdtp JSON parser accepted C-style comments: SkipComment recognized // and /* ... */ runs, and SkipWhitespaceAndComments called it before every token in ParseToken and again after a parsed value in the second hunk’s value-scanning routine. This meant the parser accepted documents that are not valid JSON, diverging from the standard, from JSON.parse(), and from what crdtp itself ever emits, violating the invariant that the CDP parser and standard JSON consumers agree on the same bytes. Such leniency is a parser-differential source: comment bytes are stripped only on the crdtp side, so the same message can decode to different structures across parsing layers.
The fix removes SkipComment entirely and replaces SkipWhitespaceAndComments with SkipWhitespace, which only advances over true whitespace (IsSpaceOrNewLine); a / is no longer swallowed, so it now terminates whitespace-skipping and is parsed as an ordinary (invalid) token. This realigns the parser with RFC 8259 and JSON.parse(), closing the divergence.
crdtp JSON parser deviated from the JSON standard by silently accepting comments no compliant consumer accepts; the fix deletes all comment handling so the parser matches JSON.parse() and the differential disappears.Attack Path
- Craft a CDP message with comments
An attacker who can feed JSON into the
crdtpparser embeds//or/* ... */sequences that the lenient parser tolerates but standard JSON parsers reject. - Create a parse divergence
Because comment bytes are consumed only by
crdtp’sSkipWhitespaceAndComments, the message decodes to one structure viacrdtpand a different one (or a parse error) via a standards-compliant parser. - Trigger improper output encoding (inferred) A DevTools component that later re-encodes or re-parses the message with a standard parser sees the mismatched structure, producing the “improper output encoding” the advisory names.
Impact Assessment
crdtp JSON parsing path. The patch demonstrates only the removal of comment leniency and no memory-safety primitive, so any escalation beyond the encoding/logic divergence is (inferred). The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcrdtp/json.cc |
modified | |
forcrdtp/json.cc |
modified | |
whilecrdtp/json.cc |
modified |
Files Changed
crdtp/json.cc
Audit Directions
- Non-standard parser leniencyaudit other hand-written parsers (JSON, CBOR, URL, header, cookie) for accepting constructs the corresponding canonical parser rejects, especially comments, trailing commas, and duplicate keys.
- Parser differentialsfind places where the same bytes are decoded by two parsers (
crdtpvs V8JSON.parse, serializer vs deserializer) and confirm the two agree, including on error inputs. - Serialize/parse round-trip asymmetryflag parsers that accept inputs their own serializer never emits, since that emit/accept mismatch is exactly where smuggling and encoding bugs hide.
Patch
From b8141806c6b0382b0b741d5c23d45c85168644e6 Mon Sep 17 00:00:00 2001 From: Andrey Kosyakov <[email protected]> Date: Mon, 14 Sep 2026 12:05:38 -0700 Subject: [PATCH] Do not accept comments in JSON Standard does not allow them, `JSON.parse()` does not accept them, we do not emit them, we do not even test them. Bug: 553116160 Change-Id: Ide07cadae113160a27687b2b4b130baec3ce839d --- diff --git a/crdtp/json.cc b/crdtp/json.cc index bc3b8a6..c7a9694 100644 --- a/crdtp/json.cc +++ b/crdtp/json.cc @@ -606,74 +606,26 @@ return false; } - static bool SkipComment(const Char* start, - const Char* end, - const Char** comment_end) { - if (start == end) - return false; - - if (*start != '/' || start + 1 >= end) - return false; - ++start; - - if (*start == '/') { - // Single line comment, read to newline. - for (++start; start < end; ++start) { - if (*start == '\n' || *start == '\r') { - *comment_end = start + 1; - return true; - } - } - *comment_end = end; - // Comment reaches end-of-input, which is fine. - return true; - } - - if (*start == '*') { - Char previous = '\0'; - // Block comment, read until end marker. - for (++start; start < end; previous = *start++) { - if (previous == '*' && *start == '/') { - *comment_end = start + 1; - return true; - } - } - // Block comment must close before end-of-input. - return false; - } - - return false; - } - static bool IsSpaceOrNewLine(Char c) { // \v = vertial tab; \f = form feed page break. return c == ' ' || c == '\n' || c == '\v' || c == '\f' || c == '\r' || c == '\t'; } - static void SkipWhitespaceAndComments(const Char* start, - const Char* end, - const Char** whitespace_end) { - while (start < end) { - if (IsSpaceOrNewLine(*start)) { - ++start; - } else if (*start == '/') { - const Char* comment_end = nullptr; - if (!SkipComment(start, end, &comment_end)) - break; - start = comment_end; - } else { - break; - } + static void SkipWhitespace(const Char* start, + const Char* end, + const Char** whitespace_end) { + while (start < end && IsSpaceOrNewLine(*start)) { + ++start; } *whitespace_end = start; } - static Token ParseToken(const Char* start, - const Char* end, - const Char** tokenStart, - const Char** token_end) { - SkipWhitespaceAndComments(start, end, tokenStart); + Token ParseToken(const Char* start, + const Char* end, + const Char** tokenStart, + const Char** token_end) { + SkipWhitespace(start, end, tokenStart); start = *tokenStart; if (start == end) @@ -999,7 +951,7 @@ return; } - SkipWhitespaceAndComments(token_end, end, value_token_end); + SkipWhitespace(token_end, end, value_token_end); } void HandleError(Error error, const Char* pos) {