How HTML Sanitizer Bypass via XMP Tag Passthrough Happens in Node.js and How to Fix It
The Real-World Problem
In a Node.js application handling user-generated content, developers rely on the sanitize-html library to strip dangerous scripts and malicious markup before displaying user input in browsers. It's a critical line of defense against stored XSS attacks. But in CVE-2026-44990, that defense had a critical flaw: the library failed to properly sanitize content within the <xmp> (example text) element, allowing attackers to inject executable scripts that would run in other users' browsers.
This vulnerability affected any application using sanitize-html versions before 2.17.4. The attack was simple but devastating—a single malicious HTML snippet could compromise the security of an entire user base.
Understanding the Vulnerability
What Is the XMP Tag and Why Does It Matter?
The <xmp> tag is an obsolete but still-parsed HTML element that displays text in a monospace font without interpreting any markup inside it. From a parser's perspective, everything between <xmp> and </xmp> is treated as raw text—no nested tags are processed.
This is where the vulnerability lies: the sanitize-html library didn't account for this raw-text parsing behavior.
The Attack Pattern
Consider this malicious input that an attacker might submit as a user comment or forum post:
<xmp><script>alert('XSS')</script></xmp>
In a naive sanitizer, the logic might work like this:
- Parse the HTML
- See
<xmp>tag—is it in the allowlist? ✓ (it's a harmless display element) - Process content inside:
<script>alert('XSS')</script> - See
<script>tag—is it in the allowlist? ✗ (scripts are dangerous, strip it) - Output:
<xmp></xmp>(script removed, safe!)
But here's the problem: Because <xmp> is a raw-text element, the HTML parser never actually interprets the <script> tag as a tag at all—it's just raw text content. When the browser renders this, it sees:
<xmp><script>alert('XSS')</script></xmp>
The browser's HTML parser recognizes <xmp> as a raw-text element, so it treats <script>alert('XSS')</script> as literal text to display... but modern browsers also have a quirk: they execute scripts that appear textually inside <xmp> tags in certain contexts.
The result: the script executes despite being "sanitized."
Why This Matters for Your Application
If your Node.js application uses sanitize-html to process:
- User comments or forum posts
- Rich text editor content
- Imported HTML from external sources
- User profile descriptions
...then this vulnerability could allow an attacker to:
- Steal session cookies and hijack user accounts
- Deface content visible to other users
- Perform actions on behalf of logged-in users
- Redirect users to malicious sites
- Harvest credentials through fake login forms
The Vulnerability in the Code
The vulnerable version (2.17.3) of sanitize-html didn't have proper handling for raw-text elements. When processing HTML like this:
const sanitizeHtml = require('sanitize-html');
// User-supplied content
const userInput = '<xmp><script>alert("XSS")</script></xmp>';
// Application attempts to sanitize it
const safe = sanitizeHtml(userInput, {
allowedTags: ['xmp', 'p', 'br'],
allowedAttributes: {}
});
console.log(safe); // Still contains the script in 2.17.3!
The library would output the dangerous content because it didn't properly parse raw-text elements. The <xmp> tag was treated as a simple container, and the script inside wasn't properly neutralized.
The Fix: Upgrading to sanitize-html 2.17.4
The fix was straightforward but critical: upgrade the sanitize-html dependency from 2.17.3 to 2.17.4.
Changes Made
In client/package.json:
- "sanitize-html": "2.17.3",
+ "sanitize-html": "2.17.4",
In pnpm-lock.yaml:
sanitize-html:
- specifier: 2.17.3
- version: 2.17.3
+ specifier: 2.17.4
+ version: 2.17.4
Additionally, the lock file shows that version 2.17.4 introduced new dependencies:
+ dayjs@1.11.21:
+ resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
+ launder@1.7.1:
+ resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==}
These new dependencies support improved parsing and sanitization logic.
What Changed in 2.17.4
The new version implements proper handling for raw-text elements (<xmp>, <script>, <style>, <textarea>, etc.) by:
- Recognizing raw-text element boundaries correctly — The parser now understands that content inside these elements should not be parsed as HTML tags
- Applying sanitization before raw-text processing — Content is sanitized before being placed into raw-text containers
- Stripping dangerous raw-text elements entirely — By default,
<xmp>and<script>are removed unless explicitly whitelisted with proper content validation
With the fix in place:
const sanitizeHtml = require('sanitize-html'); // 2.17.4
const userInput = '<xmp><script>alert("XSS")</script></xmp>';
const safe = sanitizeHtml(userInput, {
allowedTags: ['xmp', 'p', 'br'],
allowedAttributes: {}
});
console.log(safe); // Output: '' (xmp removed by default, or '<xmp></xmp>' if configured)
The script is now properly neutralized.
Why This Matters: Real-World Impact
This vulnerability was actively exploitable and could have affected:
- E-commerce platforms storing user reviews
- Social media applications processing user posts
- Documentation systems accepting user-contributed content
- CMS platforms with rich text editors
- Support ticket systems with HTML formatting
An attacker could create a single malicious post that would execute JavaScript in every user's browser who viewed it—a stored XSS attack with maximum impact.
Prevention & Best Practices
1. Keep Sanitization Libraries Updated
Security vulnerabilities in sanitizers are discovered regularly. Make updating these libraries a priority:
# Check for outdated packages
npm outdated
# Update specific package
npm update sanitize-html
# Or use pnpm
pnpm update sanitize-html
2. Use Strict Allowlists, Not Blocklists
Always configure sanitize-html with an explicit allowlist of permitted tags and attributes:
// ✓ Good: Explicit allowlist
const safe = sanitizeHtml(userInput, {
allowedTags: ['p', 'br', 'strong', 'em', 'u'],
allowedAttributes: {},
disallowedTagsMode: 'discard'
});
// ✗ Bad: Relying on defaults or blocklists
const unsafe = sanitizeHtml(userInput); // May allow dangerous tags
3. Combine with Content Security Policy (CSP)
Even with sanitization, add defense-in-depth with CSP headers:
// Express example
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
next();
});
CSP prevents inline scripts from executing even if they bypass sanitization.
4. Validate Input Type and Format
Don't assume user input is HTML:
// Validate that input is actually a string and reasonable length
if (typeof userInput !== 'string' || userInput.length > 10000) {
throw new Error('Invalid input');
}
const safe = sanitizeHtml(userInput);
5. Use Automated Dependency Scanning
Integrate tools into your CI/CD pipeline to catch vulnerable dependencies:
# Example GitHub Actions workflow
- name: Run security scan
run: |
npm install -g trivy
trivy fs . --severity CRITICAL,HIGH
6. Understand Raw-Text Elements
Be aware of HTML elements that behave differently:
- <script> — Content is not parsed as HTML
- <style> — Content is not parsed as HTML
- <xmp> — Content is displayed as-is (obsolete but still parsed)
- <textarea> — Content is not parsed as HTML
- <iframe> — Content is parsed as a separate document
These require special handling in sanitizers.
Key Takeaways
- Never trust user-supplied HTML — Always sanitize with a maintained, up-to-date library
- Raw-text elements are a blind spot — Sanitizers must explicitly handle
<xmp>,<script>,<style>, and similar elements correctly - Update sanitize-html immediately — If you're on 2.17.3 or earlier, upgrade to 2.17.4 or later
- Defense-in-depth is essential — Combine sanitization with CSP, input validation, and output encoding
- Automated scanning catches these — Security scanners like Trivy detected this vulnerability before it could cause widespread damage
How Orbis AppSec Detected This
Source: User-generated HTML content passed to the sanitize-html library in client/package.json (stored in user comments, posts, or rich text fields)
Sink: The sanitize-html function call that processes untrusted HTML input without proper raw-text element handling in sanitize-html versions < 2.17.4
Missing control: The vulnerable version didn't properly parse and sanitize content within raw-text elements like <xmp>, allowing scripts to bypass the sanitization filter
CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation – 'Cross-site Scripting')
Fix: Upgrade sanitize-html from 2.17.3 to 2.17.4, which implements proper handling for raw-text elements and prevents script injection through XMP tag passthrough
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-44990 is a stark reminder that even well-maintained security libraries can have blind spots. The <xmp> tag bypass in sanitize-html demonstrates how edge cases in HTML parsing can create critical vulnerabilities.
The good news: this fix is simple and non-breaking. Upgrading to sanitize-html 2.17.4 eliminates the vulnerability without changing your application's intended behavior. All existing tests pass, meaning the fix is a safe, drop-in replacement.
If you're using sanitize-html in a Node.js application, update your dependencies today. Then, implement the best practices outlined above to create defense-in-depth against XSS attacks. Security is not a single library or tool—it's a combination of proper tools, configuration, and practices.
References
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- OWASP: Cross-Site Scripting (XSS)
- OWASP: Cross Site Scripting Prevention Cheat Sheet
- sanitize-html npm package documentation
- Content Security Policy (CSP) MDN Documentation
- Semgrep rule for XSS detection
- fix: upgrade sanitize-html to 2.17.4 (CVE-2026-44990)