v2.0.0
Shipped 26 July 2026. Auditing my own benchmark turned up seven defects in the
published 1.0.0 — the worst being that the named exports threw in ESM and, in
CommonJS, silently sanitized with no default options, and that isDangerous()
returned false for genuinely dangerous input on every second call.
Details below; the short version is that the library is now correct, and the comparison it used to advertise is gone.
The problem
Every time you render user-generated content — comments, rich text, profile bios — you open a door. Sanitization is the standard fix, but the popular libraries leak. Polyglot payloads, encoding bypasses, and namespace-confusion techniques slip past both DOMPurify and sanitize-html.
Purifai exists to close that gap: an ultra-secure, zero-dependency HTML sanitizer that is TypeScript-native, roughly 12KB, and runs in both Node.js and the browser.
The benchmark was measuring the wrong thing
The original suite scored a vector as "blocked" when the output was empty. I tested that rule against three implementations on the same 64 vectors:
| Implementation | Score |
|---|---|
| Purifai | 64/64 (100%) |
() => '' — deletes everything, sanitizes nothing | 64/64 (100%) |
v => v — identity, no sanitization at all | 2/64 |
A function returning the empty string ties a perfect score. The metric rewarded
deletion, not safety — and it penalised competitors for correct behaviour, since
it flagged any surviving <svg> or <math> even though those are in DOMPurify's
allow-list. So I replaced it.
Results
The new benchmark inserts each sanitizer's output into a real DOM (jsdom), then serializes and re-parses it — the round trip where mutation XSS lives — and scores two axes, because security alone is meaningless without knowing what survived:
| Library | Category | Security | Text kept | Markup kept |
|---|---|---|---|---|
| Purifai | strip-to-text | 100% | 100% | 0% (by design) |
| DOMPurify | preserve-html | 100% | 100% | 100% |
| sanitize-html | preserve-html | 100% | 100% | 100% |
| xss | preserve-html | 100% | 100% | 100% |
84 attack vectors (OWASP, PortSwigger, cure53 mXSS/namespace-confusion corpora).
The honest read: on security there is no headroom — every maintained sanitizer blocks everything. Purifai isn't "more secure than DOMPurify". It's smaller (4.1 KB gzipped), dependency-free, and needs no DOM, so it runs in edge and worker runtimes where jsdom-backed sanitizers can't.
Using it
npm install purifai
import { Purifai } from 'purifai'; const clean = Purifai.sanitize('<script>alert("xss")</script>Hello World'); // -> "Hello World" const safe = Purifai.sanitize(userInput, { maxLength: 10000, allowBasicHtml: false, aggressiveMode: true, });
No DOM shim, no external dependencies — a function that takes dirty HTML and returns safe output.
What I'd highlight
The most useful work was auditing my own claims. Fuzzing with jsdom as an oracle —
generated payloads checked against six invariants rather than against another
regex — found a defect no curated corpus had: the cleanup pass used
/on\w+\s*=/gi, and since \w matches digits with no word boundary, the
pipeline's own earlier deletions could turn confirm(1)href= into
confirm1href=, whose onfirm1href= substring then matched. Removal was
manufacturing matches absent from the input. Fixing it meant a word boundary
plus iterating filters to a fixpoint — which makes idempotence a security property
for a delete-based design, not a nicety.
The broader lesson: a benchmark you wrote yourself will flatter you unless you
calibrate it. Adding () => '' and v => v as rows is what exposed the original
metric, and it's now a permanent part of the suite so the number can be audited by
anyone reading it.
