Parser accepts invalid UTF-16 surrogate pairs (low-low) as valid #39

Closed
opened 2026-06-29 00:10:36 +00:00 by weaselbot · 1 comment
Member

The string unescape logic treats any code unit in the surrogate range (0xD800-0xDFFF) as the start of a surrogate pair, and only verifies that the second code unit is a low surrogate (0xDC00-0xDFFF). This allows two consecutive low surrogates (e.g. \uDC00\uDC00) to be accepted and decoded as a valid code point, which violates RFC 8259 and the project's stated RFC 8259 conformance ("Invalid escaped UTF-16 surrogate pairs are rejected" in README.md).

Affected code:

  • src/parser3.h lines 628-636 (fast path in n_string2):

    if (0xd800 <= codepoint && codepoint <= 0xdfff) {
      int32_t codepoint2 = read4_hex(buf + 2);
      if (!(buf[0] == '\\' && buf[1] == 'u' && 0xdc00 <= codepoint2 &&
            codepoint2 <= 0xdfff)) [[unlikely]] {
        return WeaselJson_REJECT;
      }
      codepoint =
          0x10000 + (codepoint - 0xd800) * 0x400 + (codepoint2 - 0xdc00);
    

    The first check should be 0xd800 <= codepoint && codepoint <= 0xdbff.

  • src/parser3.h lines 796-798 and 855-862 (slow path in t_hex2 / t_hex3):

    if (0xd800 <= self->utf8Codepoint && self->utf8Codepoint <= 0xdfff) {
      // utf-16 surrogate
      self->utf16Surrogate = self->utf8Codepoint;
    

    and later in t_hex3:

    if (!(0xdc00 <= self->utf8Codepoint && self->utf8Codepoint <= 0xdfff))
        [[unlikely]] {
      return WeaselJson_REJECT;
    }
    

    Again, t_hex2 should only treat high surrogates (0xD800-0xDBFF) as the start of a pair.

Concrete example:

  • Input: "\uDC00\uDC00"
  • Expected: WeaselJson_REJECT (invalid surrogate pair).
  • Actual: accepted and decoded as U+10000.

A lone low surrogate followed by another low surrogate, or a high surrogate followed by another high surrogate, should be rejected. Only a high surrogate (0xD800-0xDBFF) followed by a low surrogate (0xDC00-0xDFFF) is valid.

The string unescape logic treats any code unit in the surrogate range (0xD800-0xDFFF) as the start of a surrogate pair, and only verifies that the second code unit is a low surrogate (0xDC00-0xDFFF). This allows two consecutive low surrogates (e.g. `\uDC00\uDC00`) to be accepted and decoded as a valid code point, which violates RFC 8259 and the project's stated RFC 8259 conformance ("Invalid escaped UTF-16 surrogate pairs are rejected" in README.md). Affected code: - `src/parser3.h` lines 628-636 (fast path in `n_string2`): ```cpp if (0xd800 <= codepoint && codepoint <= 0xdfff) { int32_t codepoint2 = read4_hex(buf + 2); if (!(buf[0] == '\\' && buf[1] == 'u' && 0xdc00 <= codepoint2 && codepoint2 <= 0xdfff)) [[unlikely]] { return WeaselJson_REJECT; } codepoint = 0x10000 + (codepoint - 0xd800) * 0x400 + (codepoint2 - 0xdc00); ``` The first check should be `0xd800 <= codepoint && codepoint <= 0xdbff`. - `src/parser3.h` lines 796-798 and 855-862 (slow path in `t_hex2` / `t_hex3`): ```cpp if (0xd800 <= self->utf8Codepoint && self->utf8Codepoint <= 0xdfff) { // utf-16 surrogate self->utf16Surrogate = self->utf8Codepoint; ``` and later in `t_hex3`: ```cpp if (!(0xdc00 <= self->utf8Codepoint && self->utf8Codepoint <= 0xdfff)) [[unlikely]] { return WeaselJson_REJECT; } ``` Again, `t_hex2` should only treat high surrogates (0xD800-0xDBFF) as the start of a pair. Concrete example: - Input: `"\uDC00\uDC00"` - Expected: `WeaselJson_REJECT` (invalid surrogate pair). - Actual: accepted and decoded as U+10000. A lone low surrogate followed by another low surrogate, or a high surrogate followed by another high surrogate, should be rejected. Only a high surrogate (0xD800-0xDBFF) followed by a low surrogate (0xDC00-0xDFFF) is valid.
Owner

The counterexample given doesn't actually work (it's correctly rejected), but we'll make the suggested changes anyway for clarity

The counterexample given doesn't actually work (it's correctly rejected), but we'll make the suggested changes anyway for clarity
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/weaseljson#39