Back to Blog
critical SEVERITY7 min read

How HTML sanitizer bypass via XMP tag passthrough happens in Node.js and how to fix it

A critical cross-site scripting (XSS) vulnerability in the sanitize-html library allowed attackers to bypass HTML sanitization through improper handling of the `<xmp>` raw-text element. This vulnerability could enable stored XSS attacks in any Node.js application using sanitize-html versions prior to 2.17.4. The fix involved upgrading the library to properly handle raw-text elements and prevent script injection.

O
By Orbis AppSec
Published July 27, 2026Reviewed July 27, 2026

Answer Summary

CVE-2026-44990 is a stored XSS vulnerability in Node.js's sanitize-html library (CWE-79: Improper Neutralization of Input During Web Page Generation) caused by the `<xmp>` tag being treated as a raw-text passthrough element without proper sanitization. The fix upgrades sanitize-html from 2.17.3 to 2.17.4, which adds stricter parsing rules for raw-text elements like `<xmp>`, `<script>`, and `<style>` to prevent script injection. Applications should immediately update their dependencies to patch this critical vulnerability.

Vulnerability at a Glance

cweCWE-79 (Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'))
fixUpgrade sanitize-html from 2.17.3 to 2.17.4 to implement stricter parsing and sanitization for raw-text elements
riskAttackers can inject malicious scripts that execute in the context of other users' browsers, potentially stealing session tokens, credentials, or performing unauthorized actions
languageNode.js / JavaScript
root causeThe sanitize-html library incorrectly treated the `<xmp>` raw-text element as a safe passthrough without applying proper sanitization rules, allowing embedded scripts to bypass filtering
vulnerabilityStored XSS via HTML sanitizer bypass (XMP tag passthrough)

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:

  1. Parse the HTML
  2. See <xmp> tag—is it in the allowlist? ✓ (it's a harmless display element)
  3. Process content inside: <script>alert('XSS')</script>
  4. See <script> tag—is it in the allowlist? ✗ (scripts are dangerous, strip it)
  5. 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:

  1. Recognizing raw-text element boundaries correctly — The parser now understands that content inside these elements should not be parsed as HTML tags
  2. Applying sanitization before raw-text processing — Content is sanitized before being placed into raw-text containers
  3. 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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #69036

Related Articles

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

How Cross-Site Scripting (XSS) happens in JavaScript sanitization functions and how to fix it

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.