Safeguard
AppSec

sanitize-html Vulnerabilities: History and Correct Configuration

A walk through the real npm sanitize-html vulnerabilities, from the 2016 recursion bypass to the 2024 style-attribute leak, and the configuration that keeps the library safe.

Priya Mehta
Security Analyst
7 min read

The npm sanitize-html vulnerabilities on record are not a story of a broken library, they are a catalog of the specific ways HTML sanitization goes wrong, and every one of them was fixed years before it could bite a team that upgrades promptly. sanitize-html, maintained by ApostropheCMS, is one of the most-used server-side HTML sanitizers in the Node ecosystem, and its CVE history spans a decade: recursion bypasses, entity-decoding tricks, hostname allowlist evasion, a ReDoS, and a subtle file-disclosure bug via CSS parsing. If you accept user HTML anywhere, this history is worth twenty minutes, because the same bug classes recur in every sanitizer ever written.

This guide covers each advisory, the lesson behind it, and the configuration that a security review should insist on. Current releases (2.17.0 shipped in May 2025) carry no known unfixed advisories.

Why sanitizer bugs are a distinct vulnerability class

A sanitizer sits in the most hostile position in your stack: it receives attacker-controlled markup and must transform it into something safe while preserving formatting your users care about. That means it needs a full HTML parser, entity decoding, URL parsing, and CSS parsing, and a bypass in any layer becomes stored XSS in your application.

The practical consequence: sanitizer packages generate more security releases than typical libraries, and that is a sign of scrutiny, not weakness. What matters is (a) how the maintainers respond and (b) whether your team actually picks up the patched versions. Both points are testable against the record below.

The CVE history, in order

CVE-2016-1000237: sanitize once, decode twice

Early 1.x versions did not sanitize input recursively. Markup that survived one pass could decode into executable script afterward, so an attacker could nest an encoded payload inside markup the first pass considered harmless. The fix made sanitization account for what the output would decode to. Lesson: sanitization must be idempotent, running the sanitizer on its own output should change nothing.

CVE-2017-16016: entity decoding inside nonTextTags

Versions 1.11.1 and earlier were vulnerable to XSS when at least one nonTextTags element was in play. HTML entities inside elements like textarea were decoded in a context where they became live markup. This is the classic parser-differential bug: the sanitizer and the browser disagreed about what a sequence of bytes meant.

CVE-2021-26539: internationalized domain names beat the iframe allowlist

If you used allowedIframeHostnames to restrict embeds to, say, www.youtube.com, versions before 2.3.1 could be bypassed with internationalized domain name (IDN) tricks: a hostname containing Unicode characters that normalize to something that passed the check while resolving somewhere else. Moderate severity (CVSS 5.3), but a clean illustration that hostname comparison is never simple string comparison.

CVE-2021-26540: relative URL confusion in iframes

Closely following, versions before 2.3.2 mishandled the allowIframeRelativeUrls option: URLs crafted to look relative could escape the hostname allowlist entirely. If you allow iframes at all, these two 2021 advisories are the reason your config should be reviewed by someone who has read them.

CVE-2022-25887: ReDoS in comment stripping

Versions before 2.7.1 removed HTML comments with a global regular expression whose worst-case behavior was quadratic. A single crafted payload could pin a CPU core, a denial-of-service against the exact endpoint that processes untrusted input. Rated high severity. The fix replaced the regex logic with a proper implementation. If your only mitigation for ReDoS is "we have autoscaling", read this one twice.

CVE-2024-21501: file existence disclosure via the style attribute

The most instructive recent bug. When sanitize-html runs on the backend with the style attribute allowed, versions before 2.12.1 could disclose whether files existed on the server. The path ran through PostCSS sourcemap handling: crafted CSS in a style attribute caused the CSS parser to probe filesystem paths. Moderate severity, fixed in 2.12.1 (February 2024).

The lesson generalizes: every attribute you allow drags a parser into your trust boundary. Allowing style means running a CSS parser on attacker input, with whatever behaviors that parser has.

The configuration that avoids the traps

sanitize-html's defaults are sensible, but real applications loosen them. Here is a configuration pattern that reflects the CVE history above:

const sanitizeHtml = require('sanitize-html');

const clean = sanitizeHtml(dirty, {
  allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li',
                'blockquote', 'code', 'pre', 'h2', 'h3'],
  allowedAttributes: {
    a: ['href', 'name', 'target', 'rel'],
  },
  allowedSchemes: ['https', 'mailto'],
  // If you must allow iframes, pin exact hostnames and disallow relative URLs
  allowedIframeHostnames: ['www.youtube-nocookie.com'],
  allowIframeRelativeUrls: false,
  disallowedTagsMode: 'discard',
});

The rules encoded there:

  1. Allowlist tags and attributes explicitly. Never pass through "everything except scripts". Every bypass in this history rode on something that was allowed.
  2. Do not allow style unless a designer can articulate why, and if they can, upgrade past 2.12.1 and treat the CSS surface as part of your threat model.
  3. Restrict URL schemes. allowedSchemes closes the javascript: family of tricks on href.
  4. Pin iframe hostnames exactly and keep allowIframeRelativeUrls off. Prefer the privacy domains (youtube-nocookie.com) while you are at it.
  5. Run on current versions. Everything above is fixed in 2.12.1 and later; the 2.15–2.17 line added useful hardening hooks like onOpenTag events and the excludeTag return value for surgical filtering.

Keeping the dependency patched, not just configured

Configuration prevents the misuse bugs; only upgrades prevent the parser bugs. Two operational habits close the gap:

  • Continuous SCA scanning. sanitize-html frequently arrives transitively, through markdown renderers, comment systems, or CMS packages, so teams are often exposed without ever writing require('sanitize-html'). A software composition analysis tool such as Safeguard's SCA flags the vulnerable range wherever it hides in the tree and maps it to the fixed version.
  • DAST on the endpoints that accept HTML. Sanitizer bypasses are exactly what dynamic scanning exists to catch: probing the rendered output with the entity, IDN, and scheme tricks above tells you whether your configuration holds in your app, not just in the library's test suite.

If your codebase still pins a 1.x version, treat it as an incident, not a chore: 1.x predates every fix in this article.

How sanitize-html compares to DOMPurify

The perennial question. DOMPurify sanitizes against a real browser DOM (via jsdom on the server), which eliminates whole classes of parser-differential bugs but brings jsdom's own weight and history. sanitize-html parses with htmlparser2, which is fast and battle-tested but means the sanitizer's view of the markup is not literally the browser's view.

Both are legitimate choices with active maintenance and honest advisory histories. What is not legitimate is a hand-rolled regex sanitizer; the ReDoS and entity bugs above were written by people who think carefully about HTML for a living. Pick one of the two, configure it tightly, and keep it patched.

FAQ

Is sanitize-html safe to use right now?

Yes. All published advisories, from CVE-2016-1000237 through CVE-2024-21501, are fixed in current releases (2.12.1 covered the last one; the 2.17.x line is current as of mid-2025). The risk today comes from running old versions or over-permissive configuration.

What is the most serious sanitize-html vulnerability to date?

CVE-2022-25887, the ReDoS in HTML comment removal fixed in 2.7.1, carried the highest severity rating. For data impact, CVE-2024-21501 matters most to backend deployments because it could disclose file existence on the server when the style attribute was allowed.

Should I allow the style attribute in sanitize-html?

Avoid it unless you have a concrete need. CVE-2024-21501 showed that allowing style pulls a CSS parser into the attack surface. If you need inline styling, upgrade to 2.12.1 or later and constrain allowed CSS properties as tightly as the API permits.

How do I know if sanitize-html is in my dependency tree?

Run npm ls sanitize-html in each service, and back it with an SCA scanner for continuous coverage, since the package commonly arrives transitively via markdown, comment, or CMS libraries rather than as a direct dependency.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.