The xss npm package (project name js-xss, by Zongmin Lei) sanitizes untrusted HTML against an explicit tag-and-attribute whitelist, making it one of the standard choices for cleaning user-supplied rich text in Node.js before storage or rendering. The library's model is simple and sound: everything is forbidden unless the whitelist allows it. Most real-world failures with it are not library bugs — they are integration bugs, where the sanitizer runs in the wrong place, with a loosened whitelist, or against the wrong output context. This guide covers correct usage, the configuration options that carry sharp edges, and how it compares to DOMPurify and sanitize-html.
What it does
Install and call filterXSS (also the default export):
npm install xss
const xss = require('xss');
const dirty = '<div onclick="alert(1)"><script>steal()</script><b>hello</b></div>';
const clean = xss(dirty);
// '<div><script>steal()</script><b>hello</b></div>'
Out of the box: tags not on the whitelist are HTML-escaped (not removed — note the encoded script tag above), event-handler attributes are stripped, and attribute values pass through safeAttrValue, which blocks javascript: and other script-bearing URL schemes in href and src. The default whitelist covers the tags you would expect in user-generated rich text — paragraphs, headings, lists, links, images, tables — each with a minimal attribute set.
The npm xss package runs in Node and the browser, has no dependencies beyond the maintainer's own cssfilter and commander, and even ships a CLI (xss -t input.html) for spot-checking. The latest release, 1.0.15, has no known vulnerabilities in the public advisory databases at the time of writing; older versions do have reported filter-bypass advisories, so this is a package to keep current rather than pin and forget. Release cadence is slow — years between versions — which for a mature sanitizer is not by itself alarming, but it does mean you should watch the advisory feed rather than assume patches will be proactive.
Configure by tightening, not loosening
Customization happens through an options object, and the security posture of your integration is roughly "the default whitelist, minus what you remove, plus what you add." Additions are where risk enters.
const xss = require('xss');
const sanitizer = new xss.FilterXSS({
whiteList: {
a: ['href', 'title', 'target', 'rel'],
p: [], b: [], i: [], em: [], strong: [],
ul: [], ol: [], li: [],
code: [], pre: [],
},
stripIgnoreTag: true, // remove non-whitelisted tags entirely
stripIgnoreTagBody: ['script', 'style'], // and drop these tags' contents
});
const clean = sanitizer.process(userInput);
Decisions embedded in that config, each worth making deliberately:
stripIgnoreTag: trueremoves disallowed tags instead of escaping them. Escaping (the default) preserves content visibly — users see their attempted markup as text — while stripping produces cleaner output. Either is safe; mixing expectations between frontend and backend is what causes double-encoding bugs.stripIgnoreTagBody: ['script', 'style']drops the contents of those tags, not just the tags. Without it,<script>bodies survive as escaped text, which is safe but ugly.- Build the whitelist from empty, per feature. A comment box needs less than a CMS body field. Two sanitizer instances with two whitelists beat one permissive global.
The dangerous options are the escape hatches: onTag, onTagAttr, and onIgnoreTagAttr callbacks let you override decisions per-node, and returning a raw string from them bypasses filtering for that node entirely. Nearly every "we use the xss package but still got XSS" postmortem involves a callback that allowlisted something clever — an iframe for embeds, a style attribute for the editor — without reproducing the value validation the library would have applied. If you allow style, keep the companion css: {} filtering (backed by cssfilter) enabled rather than switching it off for convenience.
The pitfalls that actually cause incidents
Sanitizing for the wrong context. An HTML sanitizer makes untrusted content safe to place in HTML element content. It does not make content safe inside an attribute you concatenate yourself, inside a <script> block, inside a URL you build, or inside CSS. If a template does href="${cleaned}", sanitization happened but the injection is in URL context and the defense does not transfer. Sanitize for the body, encode per-context everywhere else.
Sanitizing on input only. Storing sanitized HTML and trusting it forever means one whitelist bug — or one legacy record written before the sanitizer existed — persists as stored XSS. The resilient pattern is sanitize on write and on render, with render as the one you rely on. CPU cost is negligible next to the incident it prevents.
Client-side sanitization as the only line. Anything running in the browser can be bypassed by posting directly to your API. Sanitizing in React before display is good defense-in-depth; the authoritative pass belongs on the server. This split is also where testing catches drift: a DAST scan that submits payloads through the real API and inspects rendered responses verifies the server-side pass exists, which no amount of frontend review can.
Mutation and parser-differential tricks. Whitelist sanitizers that regex-and-rewrite HTML (rather than fully parsing to a DOM) have historically been targets for mutation XSS — payloads that are inert as written but become active after a browser re-serializes them. This class of attack is the main argument for DOMPurify in browser contexts, since it sanitizes an actual DOM tree. For js-xss on the server, mitigate by keeping the whitelist tight (mXSS vectors overwhelmingly need unusual tags like svg, math, noscript, template — do not allow them) and keeping the package updated.
Choosing between xss, DOMPurify, and sanitize-html
- DOMPurify — the default choice in browsers, and on the server via jsdom. DOM-based sanitization gives the strongest mXSS resistance; the project is actively maintained with fast turnaround on reported bypasses.
- sanitize-html — Node-focused, parser-based (htmlparser2), very configurable transform pipeline. Heavier dependency tree than xss.
- xss (js-xss) — lightweight, fast, whitelist-first ergonomics, dual Node/browser support. Good fit when you want a small dependency and a strict whitelist and you control both ends of the pipeline.
Whichever you choose, treat the sanitizer as critical infrastructure in your dependency inventory: subscribe to its advisories, and let your SCA tooling — Safeguard or equivalent — flag when a bypass is published, because sanitizer CVEs are the kind where "we'll upgrade next sprint" is the wrong answer. And remember the naming hazard when installing: the package is literally named xss, which makes typos and lookalike packages (xss-filters, x-s-s, and friends) easy to confuse in a rushed npm install. Verify the author and repository before first install, as you would for any security-critical package.
FAQ
What does the xss npm package do?
It sanitizes untrusted HTML against a configurable whitelist of tags and attributes: disallowed tags are escaped or stripped, event-handler attributes are removed, and URL attributes are checked for script-bearing schemes like javascript:. It works in Node.js and browsers.
Is the xss package still maintained?
It is mature and slow-moving — version 1.0.15 is the latest, published in 2023, with no known vulnerabilities against it at the time of writing. Treat it as stable but monitor advisories, since older versions did receive bypass reports and fixes arrive with the ecosystem's attention, not on a schedule.
Should I use xss or DOMPurify?
In the browser, DOMPurify — its DOM-based approach resists mutation-XSS tricks better and it is very actively maintained. On the server, js-xss is a reasonable lightweight choice with a strict whitelist; DOMPurify-with-jsdom or sanitize-html are the alternatives when you need stronger guarantees or richer transforms.
Does sanitizing input once make stored content safe forever?
No. Whitelist bugs get discovered, requirements change, and pre-sanitizer legacy data lingers. Sanitize on write for hygiene, but perform the authoritative sanitization at render time so a single historical gap does not become permanent stored XSS.