fix(tokenizer): treat `<![CDATA[` as a bogus comment in HTML mode
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
- §13.2.5.42 Markup declaration open state:
<![CDATA[is only consumed as a CDATA section when there is an adjusted current node that is not an element in the HTML namespace (foreign content); otherwise it is a parse error and the tokenizer switches to the bogus comment state. - §13.2.5.43 Bogus comment state: a bogus comment ends at the first
>.
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 liveimgelement (mirrors the parse5 tree above; fails onmaster, passes with this change); - two regression guards confirming real CDATA in foreign content and in
xmlModeis 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
recognizeCDATAoption to control CDATA recognition, enabled by default in XML mode.
- Added a