Severity by source
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
Primary rating from Vendor (https://github.com/apostrophecms/apostrophe).
CVSS VectorVendor: https://github.com/apostrophecms/apostrophe
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
Lifecycle Timeline
3Blast Radius
ecosystem impact- 56 npm packages depend on sanitize-html (13 direct, 43 indirect)
Ecosystem-wide dependent count for version 2.17.3.
DescriptionCVE.org
Summary
Under the default configuration, sanitize-html can turn attacker-controlled content inside a disallowed xmp element into live HTML or JavaScript. This is a sanitizer bypass in the default disallowedTagsMode: 'discard' path and can lead to stored XSS in applications that render sanitized output back to users.
Details
In sanitize-html@2.17.3, the default nonTextTags list includes only script, style, textarea, and option in index.js lines 138-142. That means disallowed xmp tags are not treated as "drop the entire contents" tags.
Later, in the ontext handler at index.js lines 569-577, the code special-cases textarea and xmp and appends their text content directly to the output without escaping:
} else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (tag === 'textarea' || tag === 'xmp')) {
result += text;
}Because htmlparser2 treats xmp as a raw-text element, markup inside xmp is parsed as text on input but becomes live markup again once it is appended unescaped to the sanitized output.
This creates a default sanitizer bypass. For example, a disallowed <xmp> wrapper can be used to smuggle <script> or event-handler payloads through sanitization.
The README also appears to contradict the implementation. In the "Discarding the entire contents of a disallowed tag" section, the documented exception list names only style, script, textarea, and option, and does not mention xmp.
PoC
Tested locally against sanitize-html@2.17.3 on Node.js v25.2.1.
- Install the package:
npm install sanitize-html- Run the following script:
const sanitizeHtml = require('sanitize-html');
console.log(sanitizeHtml('<xmp><script>alert(1)</script></xmp>'));
console.log(sanitizeHtml('<xmp><img src=x onerror=alert(1)></xmp>'));
console.log(sanitizeHtml('<xmp><svg><script>alert(1)</script></svg></xmp>'));- Observed output:
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg><script>alert(1)</script></svg>- Render any of the returned strings in a browser context that trusts
sanitize-htmloutput, for example:
const dirty = '<xmp><script>alert(1)</script></xmp>';
const clean = sanitizeHtml(dirty);If clean is inserted into the DOM or stored and later rendered as trusted HTML, the attacker-controlled script executes.
Impact
This is a cross-site scripting vulnerability in the default sanitizer behavior. Any application that uses sanitize-html defaults and then renders the returned HTML as trusted output is impacted. A remote attacker who can submit HTML content can trigger execution of arbitrary JavaScript in another user's browser when that content is viewed.
AnalysisAI
The sanitize-html npm package allows remote attackers to bypass HTML sanitization and inject executable JavaScript by wrapping malicious payloads inside disallowed <xmp> tags, achieving stored cross-site scripting (XSS) in applications using default configurations. This affects all versions through 2.17.3, with no vendor-released patch identified at time of analysis. A publicly available proof-of-concept demonstrates the bypass, which leverages the library's special handling of raw-text elements. With a 9.3 CVSS score and network-based attack vector requiring only user interaction, this represents a critical risk for Node.js applications that render sanitized user content in browsers.
Technical ContextAI
The vulnerability stems from an incomplete implementation of HTML5 raw-text element handling in sanitize-html's parser integration with htmlparser2. The library's default nonTextTags list (index.js lines 138-142) includes only script, style, textarea, and option tags for complete content dropping, but omits the xmp element. However, the ontext handler (lines 569-577) explicitly special-cases both textarea and xmp, appending their text content directly to output without HTML entity encoding. Because htmlparser2 correctly treats xmp as a raw-text element per HTML5 parsing rules, markup inside xmp is parsed as text nodes during sanitization but becomes live executable HTML when the unescaped text is reassembled in the output string. This creates a context-switching bypass where disallowed tags like script can be smuggled through sanitization wrapped in an outer xmp container that itself gets discarded while its 'text' payload survives unescaped. The vulnerability is a classic double-parsing or content-type mismatch issue rooted in CWE-79 (Improper Neutralization of Input During Web Page Generation).
RemediationAI
No vendor-released patch identified at time of analysis for sanitize-html versions <= 2.17.3. Organizations should monitor the project's GitHub repository (https://github.com/apostrophecms/sanitize-html) and advisory GHSA-rpr9-rxv7-x643 for patch release announcements. As an immediate compensating control, add 'xmp' to the nonTextTags configuration array to force complete content dropping for xmp elements, preventing the unescaped text passthrough: sanitizeHtml(input, { nonTextTags: ['script', 'style', 'textarea', 'option', 'xmp'] }). This configuration change aligns xmp handling with the library's treatment of other raw-text elements and blocks the bypass vector. Trade-off: requires code changes to every sanitize-html invocation if using a wrapper function, or modification of application-wide sanitizer configuration. Alternative mitigation: implement Content Security Policy (CSP) with strict-dynamic and nonce-based script whitelisting in the browser layer to prevent execution of injected inline scripts even if sanitization is bypassed; however, this does not prevent DOM-based XSS via event handlers like onerror, so CSP alone is insufficient. For applications unable to patch immediately, consider replacing sanitize-html with DOMPurify (isomorphic-dompurify for Node.js) or implementing server-side rendering with context-aware output encoding.
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-36566
GHSA-rpr9-rxv7-x643