XSS Flaw in the Popular HTML Sanitizer sanitize-html: Update to 2.17.4 — CVE-2026-44990
A vulnerability (CVE-2026-44990, CVSS 9.3) was found in sanitize-html, the go-to HTML sanitizer for preventing XSS, using the deprecated <xmp> tag to slip past sanitization. It is downloaded 7M+ times a week; the fix is updating to 2.17.4. Here is how it works and what to check.

Makoto Horikawa
Backend Engineer / AWS / Django
A vulnerability (CVE-2026-44990, CVSS 9.3) was found in sanitize-html, the go-to HTML sanitizer for preventing XSS, using the deprecated <xmp> tag to slip past sanitization. It is downloaded 7M+ times a week; the fix is updating to 2.17.4. Here is how it works and what to check.
A vulnerability has been found in sanitize-html, the go-to library for "sanitizing" user-submitted HTML by stripping out dangerous parts, that lets attacker scripts slip right through that sanitization. It is CVE-2026-44990, rated 9.3 out of 10 (Critical). A fixed version, 2.17.4, is already out, and anyone using the library is advised to update now.
sanitize-html is a widely used HTML sanitizer in the Node.js world (running JavaScript on the server). It is downloaded more than 7 million times a week on npm (JavaScript's package distribution system), making it a default choice alongside DOMPurify. Wherever rendering user-written HTML directly would be dangerous — comment sections, profiles, rich-text input — it is embedded in countless web apps as the filter that "keeps only the safe parts." It is widely used in Japanese Node.js/TypeScript services too, directly or as a transitive dependency.
What makes this serious is that it is a hole in the very part you install for safety. The sanitizer you added to prevent XSS (cross-site scripting — making an attacker's script run on someone else's site) lets XSS through when content is written a certain way. What was found this time is a simple but effective bypass abusing an old, almost-never-used HTML element, "<xmp>."
Vulnerability Overview
| Item | Detail |
|---|---|
| CVE | CVE-2026-44990 (CVSS 9.3 Critical) |
| Affected | sanitize-html before 2.17.4 (2.17.3 and earlier) |
| Fixed | 2.17.4 |
| Type | XSS (CWE-79) sanitizer bypass |
| Conditions | Default config in use a user views the content |
| Exploitation | No real attacks / not in KEV (PoC is public) |
The CVSS vector is AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N. An attacker can post malicious content over the network (AV:N) with no privileges (PR:N), and the harm lands when another user views it (UI:R). Because the scope changes (S:C), the damage crosses out of the app that ran the sanitizer and happens in the viewer's browser. This is not a server takeover (RCE); the main impact is stored XSS running script in the viewer's browser.
The crux is that a sanitizer added to prevent XSS lets XSS through with a single line using an old tag.
The fix is as easy as updating, but miss it and the hole stays open while you believe you're protected.
When the Safety Net Tears, Who Gets Hit and How
The most dangerous thing about this flaw is that the entry point is "just a post" requiring neither a login nor special privileges. The people who go after it are the anonymous users who can post to boards, comment sections, product reviews, profile fields, and contact forms; the spammers mass-producing impersonation posts; and targeted attackers aiming squarely at an administrator. What they walk off with is the viewing user's login cookie (session), the contents of a form being filled in, the personal data shown on screen, and the privileges to operate an admin panel. The moment a single line wrapped in <xmp> is saved, the attacker's script runs in the browser of whoever opens that post, and they are impersonated, session and all.
The damage downstream does not stop at one user. Impersonating someone with a stolen session hijacks their account, and if an administrator opens the post, the harm spreads across the whole site at once. The attacker can pull every user's data, rewrite page content, and use the tampered pages to deliver other malware or harvest passwords through a fake login screen. A single XSS turns into site-wide tampering and the delivery of attacks to every user — that is the danger of stored XSS.
And the responsibility for this hole returns to the operators who embedded the sanitizer believing "install it and you're safe." If a leak happens, the service provider bears the user notifications, the reports to data-protection authorities, and the work of rebuilding trust. What the CVSS 9.3 number does not show is the loss of a collapsed design premise — that the very component added for safety betrays you. That is exactly why whether you have updated to 2.17.4 now decides the safety of the service and its users.
What Does sanitize-html Actually Do?
Web services have many places where user-written text is shown directly on screen: comments, reviews, profiles, contact forms, blog bodies. If you display that input unmodified, a malicious person can slip in a <script> tag and run arbitrary script in other viewers' browsers. That is XSS.
sanitize-html is the "sieve" that prevents this. It takes the HTML a user wrote, keeps only safe tags like <b> (bold) and <a> (link), removes dangerous ones like <script>, and returns HTML that has been made safe. Developers trust this library and build their apps on the premise that "anything passed through here is safe." The problem this time is that the premise broke for certain inputs.
Why Did the Old "<xmp>" Tag Break It?
According to NVD, under the default configuration (disallowedTagsMode: 'discard'), sanitize-html can turn the contents of a disallowed <xmp> element into live HTML or JavaScript. The key is the nature of <xmp>, an old element that is barely used anymore.
<xmp> is a deprecated element that treats whatever is written inside it as "just text (raw text)." The HTML parser sanitize-html uses internally (htmlparser2) reads the contents of <xmp> as raw text, per spec, rather than as tags. But sanitize-html's "raw-text handling" wrote that content back into the output without escaping it (converting it into a harmless string). On top of that, <xmp> was missing from the default list of "tags whose contents should be treated as raw text (nonTextTags)." The result is the worst kind of bypass for a sanitizer: content read as harmless text on input turns into live HTML/JavaScript on output.
The proof of concept is simple too — a single line like <xmp><script>alert(1)</script></xmp> slips through sanitization. The fix in 2.17.4 closes the gap via the relevant commit, correctly adding <xmp> to what gets handled. This is published in the GitHub Security Advisory (GHSA-rpr9-rxv7-x643) as well. The vulnerability was reported by @sushi-gif.
Scope and Countermeasures
Affected are apps using a version of sanitize-html before 2.17.4 with the default configuration. If you take user-submitted content and display it without special options, assume you are in scope. What to do is simple.
1. Update sanitize-html to 2.17.4 or later. This is the real fix. Bump the version with npm update sanitize-html and confirm it reads 2.17.4+ including in package-lock.json. Even if you do not use it directly, another package may bundle an old sanitize-html internally (transitive dependency). Check the whole dependency tree with npm ls sanitize-html so nothing is left behind.
2. Mitigation if you can't update immediately. If an immediate update is truly hard, you can close the bypass path by explicitly adding <xmp> to the raw-text tags (nonTextTags) in your configuration. But this is a stopgap; the real fix is the version update.
3. Revisit defense in depth. "The sanitizer itself breaks" can happen, as it did here. Layering output-time escaping, a Content Security Policy (CSP — a browser-side mechanism that restricts script execution), and the HttpOnly cookie attribute — defenses that do not rely on the sanitizer alone — keeps one bypass from turning straight into damage. For a mindset on mechanically surfacing vulnerabilities in OSS dependencies, see our OSS supply-chain scanner write-up as well.
A Technical View — The Trap of "Deprecated Elements"
HTML-sanitizer bypasses are a recurring theme. Attackers hunt for the "exceptional interpretations" that browsers and HTML parsers carry. Barely-used deprecated elements like <xmp> have special spec behavior (treating contents as raw text) yet tend to fall out of the sanitizer's handling list — making them prime targets. Elements with similar properties include <noscript> and <plaintext>, and the allow/deny-list approach of a sanitizer gets broken wide open the moment it misses such corner cases.
There are two lessons. One: keep security components especially up to date. Core defenses like XSS protection get fixes fast when a bypass is found; leave them old and your "protection" becomes the biggest hole. Two: don't bet your defense on a single layer. Layer input sanitization with output escaping and a CSP, and even if the sanitizer breaks once, you can contain the damage on the spot. No real-world attack or KEV listing is confirmed this time, but with a PoC already public and the affected library so widely used, this is one to update without delay.
Summary
A vulnerability (CVE-2026-44990, CVSS 9.3) was found in sanitize-html, the go-to library for preventing XSS, that uses the deprecated old tag <xmp> to slip past sanitization and let an attacker's script through. It is a widely used component downloaded over 7 million times a week, and apps that display user posts with the default configuration are affected. The impact is stored XSS, which can chain from hijacking a viewer's session all the way to tampering with the entire site.
The fix is clear: update to 2.17.4 or later. Even if you don't use it directly, you may carry it as a transitive dependency, so check the whole dependency tree. Cases where "the component added for safety" breaks will keep happening. Rather than relying on the sanitizer alone, layer output escaping and a CSP, and keep security-related dependencies promptly up to date — that, in the end, is the surest defense.