fix(tokenizer): treat `<![CDATA[` as a bogus comment in HTML mode

Merged
SP
11d ago
+59-3

Summary

In default HTML mode, <![CDATA[ outside foreign content is tokenized with the wrong end-condition. On a full CDATA[ match, stateCDATASequence always enters the comment-like CDATA path (InCommentLike with the ]]> end sequence), so the section only ends at ]]> (or EOF). Per the WHATWG HTML spec, <![CDATA[ in this position is a bogus comment that ends at the first >, leaving everything after > as live markup.

Because of this, htmlparser2 collapses a CDATA-prefixed string plus all following markup into a single inert comment node, instead of emitting a short comment followed by the live elements.

Spec

Reproduction

import * as htmlparser2 from "htmlparser2";
const dom = htmlparser2.parseDocument("<![CDATA[x><img src=x onerror=alert(1)>");

Current master:

COMMENT "[CDATA[x><img src=x onerror=alert(1)>"   // single node, <img> never parsed

parse5 (used here as a differential spec oracle) and every browser:

COMMENT "[CDATA[x"
ELEMENT <img src=x onerror=alert(1)>              // live element

The trailing onerror attribute is only there to make the divergence visible: in the buggy output the markup after > is hidden inside a comment; in the spec output it is a real img element. This is a parser-correctness issue, not an exploit report.

Prior acknowledgement

This is the same behavior reported in #119 ("CDATA parsing is not correct when it is not in xmlMode"). The reporter verified against Gumbo and parse5 that <![CDATA[ > <div> xxx </div>]]> should yield a bogus comment <![CDATA[ > followed by live <div>xxx</div> markup, and the maintainer confirmed: "Ahh, right. I forgot it behaved this way. This will be fixed with #114." #114 (the high5 tokenizer rewrite) never landed, so the bug is still present on master.

It also runs counter to the recent spec-alignment work in #2382, #2383, and #2387.

Fix

stateCDATASequence now only takes the real-CDATA path (InCommentLike + ]]>) when CDATA should actually be recognized, via a new shouldRecognizeCDATA() helper that checks xmlMode || recognizeCDATA || isInForeignContext(). In default HTML mode it routes to InSpecialComment (bogus comment ending at >), exactly like the existing partial-match branch a few lines below already does. recognizeCDATA is read from the options already passed to the tokenizer.

xmlMode, foreign-content (SVG/MathML), and recognizeCDATA real-CDATA behavior is preserved.

Tests

Added to src/index.spec.ts:

  • a red-baseline assertion that <![CDATA[x><img src=x> in HTML mode yields a comment ending at > plus a live img element (mirrors the parse5 tree above; fails on master, passes with this change);
  • two regression guards confirming real CDATA in foreign content and in xmlMode is still recognized.

Full suite green:

Test Files  7 passed (7)
     Tests  190 passed (190)

No snapshot files changed, so existing CDATA tokenizer cases (<![CDATA[ foo ]]>, <![CD>, unclosed-at-EOF, recognizeCDATA edge cases) keep their current behavior.

Summary by CodeRabbit

  • Bug Fixes
    • CDATA-like markup in HTML is now treated as a comment unless it appears in a foreign context. XML mode continues to recognize CDATA sections.
  • New Features
    • Added a recognizeCDATA option to control CDATA recognition, enabled by default in XML mode.