Purifai

Zero-dependency, TypeScript-native strip-to-text sanitizer with contextual output encoding — and a two-axis benchmark built after I found the original one was measuring the wrong thing.

TypeScript
XSS
Security
npm
Purifai

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:

ImplementationScore
Purifai64/64 (100%)
() => '' — deletes everything, sanitizes nothing64/64 (100%)
v => v — identity, no sanitization at all2/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:

LibraryCategorySecurityText keptMarkup kept
Purifaistrip-to-text100%100%0% (by design)
DOMPurifypreserve-html100%100%100%
sanitize-htmlpreserve-html100%100%100%
xsspreserve-html100%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.