Vulnerability GHSA-x3q4-9hxx-gx8m
Summary
ApostropheCMS sanitize-html: nonTextTags discard region ends early on an implied close, letting <noscript> content render
Details
Summary
sanitize-html discards the content of every element in nonTextTags. It delimits that region using htmlparser2's close events, and ignores the isImplied flag htmlparser2 passes it. Closing a still-open ancestor from inside a <noscript> makes htmlparser2 unwind its stack past the noscript; the synthetic implied close it emits on the way ends the discard region early, and everything after it is treated as ordinary body content.
A browser with scripting enabled parses <noscript> content as raw text, so the same markup renders as inert text there. The sanitizer therefore emits elements the page itself would not have.
Reproduction
const sanitizeHtml = require("sanitize-html");
sanitizeHtml('<div><noscript></div><img src="http://evil.test/t.gif"></noscript>', {
allowedTags: ["div", "img"],
allowedAttributes: { img: ["src"] },
nonTextTags: ["script", "style", "textarea", "option", "noscript"],
});
// => '<div></div><img src="http://evil.test/t.gif" />'
Chrome, given the same input, renders no <img> — the whole payload is raw text inside the unclosed <noscript>. Verified with Playwright, srcdoc into an iframe so the scripting flag is on.
A <![CDATA[ variant hides the opening tag from the parser entirely and has the same effect:
<p>Hi</p><![CDATA[ x > <noscript>]]><a href="…">Reset your password</a><img src="…beacon.gif"></noscript>
htmlparser2 runs a CDATA token to ]]>; Chrome ends a bogus comment at the first >, so the <noscript> after it is real to the browser and invisible to the parser.
Where the behaviour is
index.js:
:154—nonTextTagsArray(the default list,['script','style','textarea','option','xmp']).:638—onclosetag: function (name, isImplied)— receives the flag.:644—skipTextDepth--— decrements without consultingisImplied.
Suggested fix
Do not end a discard region on an implied close. htmlparser2 already distinguishes them; the flag is passed and discarded. Something close to:
onclosetag: function (name, isImplied) {
if (skipText) {
if (isImplied) return; // an implied close must not end the region
skipTextDepth--;
…
Related Vulnerabilities
Other vulnerabilities affecting the same packages