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

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.