CVE-2026-113654Medium· 5.3▾ SunlitApostropheCMS sanitize-html: nonTextTags discard region ends early on an implied close, letting <noscript> content render
▾ Sunlit zone — Low / medium · no exploitation signal
impact 29.2 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
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.
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.
index.js:
:154 — nonTextTagsArray (the default list, ['script','style','textarea','option','xmp']).:638 — onclosetag: function (name, isImplied) — receives the flag.:644 — skipTextDepth-- — decrements without consulting isImplied.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--;
…
sanitize-html < 2.18.0Upgrade to a patched release:
sanitize-html 2.18.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-113661Medium· 5.4ApostropheCMS: Incomplete CVE-2026-53606: `meta http-equiv=refresh` `content` still ships `javascript:` / `data:` URLs
CVE-2026-113657Low· 3.7ApostropheCMS sanitize-html: SMIL animation namespace-prefix bypass allows javascript: URI injection (XSS)
CVE-2026-63670Medium· 6.1ApostropheCMS is an open-source Node.js content management system
CVE-2026-53606Medium· 5.4sanitize-html has incomplete URI scheme validation in that allows javascript: URIs through action, formaction, data, poster, and background attributes
CVE-2026-44990Critical· 9.3ApostropheCMS is an open-source Node.js content management system, and sanitize-html provides a simple HTML sanitizer with a clear API
CVE-2021-41164High· 8.2CKEditor4 is an open source WYSIWYG HTML editor