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.


References

Frequently Asked Questions

What is a raw-text element bypass in HTML sanitizers?

Raw-text elements like `<xmp>`, `<script>`, and `<style>` are handled specially by HTML parsers—their content is treated literally without parsing nested tags. If a sanitizer doesn't properly account for this, attackers can inject scripts inside these elements that bypass normal filtering rules.

How do you prevent XSS in Node.js when sanitizing user HTML?

Use well-maintained sanitization libraries (like sanitize-html), keep them updated to patch known bypasses, configure strict allowlists for permitted tags and attributes, and perform defense-in-depth with Content Security Policy headers.

What CWE is this vulnerability?

CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'). It's one of the most common and dangerous web vulnerabilities.

Is using a sanitization library enough to prevent XSS?

A good sanitization library is essential, but it must be kept updated. Libraries can have bugs (like this one), so combining sanitization with CSP headers, input validation, and output encoding provides stronger defense-in-depth.

Can static analysis detect this type of vulnerability?

Yes. Security scanners like Trivy can detect when known-vulnerable versions of libraries are in use. However, detecting the vulnerability within the library itself requires specialized fuzzing or manual code review of the sanitization logic.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #69036

Related Articles

critical

How DOM-based XSS via jQuery .html() happens in JavaScript and how to fix it

A critical DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in CustomRankingInterface.js where user-imported JSON data containing malicious filter names could execute arbitrary JavaScript in victims' browsers. The fix replaces jQuery's unsafe `.html()` method with the safe `.text()` method, preventing script injection while preserving the intended functionality.

critical

How reflected XSS happens in Jinja2 template rendering and how to fix it

A reflected cross-site scripting (XSS) vulnerability was discovered in the similarity search HTML template where user input from the `query` form parameter was rendered directly into an HTML attribute without proper escaping. An attacker could inject malicious JavaScript by crafting a search query containing attribute-breaking payloads like `" onfocus="alert(document.cookie)" autofocus="`, which would execute in the victim's browser.

low

When innerHTML Meets User Data: Fixing XSS Vulnerabilities in JavaScript

A low-severity Cross-Site Scripting (XSS) vulnerability was identified in `agent_chat.js`, where user-controlled data was being passed directly into DOM manipulation methods like `innerHTML`. While rated low severity, XSS vulnerabilities can be chained with other attacks to steal session tokens, redirect users, or execute arbitrary scripts in a victim's browser. The fix eliminates the unsafe pattern by replacing direct HTML injection with safer DOM manipulation techniques.

low

From text/template to html/template: Closing the XSS Door in Go

A cross-site scripting (XSS) vulnerability was discovered and patched in a Go-based application where the `text/template` package was being used instead of the safer `html/template` package for rendering HTML content. This single-line fix — swapping one import — prevents user-controlled data from being injected as raw HTML, closing a potential attack vector for malicious script injection. While rated low severity, XSS vulnerabilities are among the most common and exploitable web security issues,

medium

Wildcard PostMessage Leak: How One Character Exposed User Sessions

A critical security flaw in a browser extension's authentication flow was sending sensitive session tokens and user data to any website using the wildcard "*" origin in postMessage. This vulnerability could have allowed malicious sites to intercept authentication credentials, but was fixed by restricting message delivery to the application's own origin.

critical

Fixing Session Hijacking: From Insecure Query Parameters to Secure Sessions

A critical session management vulnerability was recently patched in our application that allowed attackers to hijack user sessions by simply manipulating URL parameters. The fix addresses both client-side XSS vulnerabilities through unsafe DOM manipulation and server-side session validation issues, demonstrating how multiple security layers work together to protect user accounts.