The problem
Every time you render user-generated content — comments, rich text, profile bios — you open a door. Sanitization is the standard fix, but preserving safe HTML and emitting plain text are different jobs with different tradeoffs.
Purifai handles the second job: a zero-dependency, TypeScript-native sanitizer that strips markup and runs in Node.js, browsers, edge runtimes, and workers without a DOM shim.
Try it
Paste markup or load a specimen below. Conversion happens locally in your browser with the published package; the page sends and stores nothing.
The interactive playground lives on worksonmy.dev.
Verification
The benchmark inserts each tool's output into a real DOM (jsdom), then serializes and re-parses it — the round trip where mutation XSS lives. It reports executable output, exact benign-text fidelity, retained markup, and raw-container body removal separately so deletion cannot masquerade as quality:
| Library | Category | No executable output* | Exact text | Markup kept | Raw body removed | Median ops/sec† |
|---|---|---|---|---|---|---|
| Purifai.sanitize | strip-to-text | 100% | 100% | 0% (by design) | 100% | 526,709 |
| Purifai.escape | encode-as-text | 100% | 40% | 0% | 0% | 596,421 |
| striptags | strip-to-text | 100% | 100% | 0% | 20% | 633,697 |
| DOMPurify (jsdom) | preserve-safe-html | 100% | 100% | 100% | 60% | 1,686 |
| sanitize-html | preserve-safe-html | 100% | 100% | 100% | 60% | 88,492 |
| xss | preserve-safe-html | 100% | 100% | 100% | 0% | 335,448 |
| rehype-sanitize | preserve-safe-html | 100% | 100% | 100% | 40% | 24,522 |
| escape-html | encode-as-text | 100% | 40% | 0% | 0% | 2,238,804 |
| validator.escape | encode-as-text | 100% | 40% | 0% | 0% | 735,565 |
| entities.escapeUTF8 | encode-as-text | 100% | 40% | 0% | 0% | 1,208,824 |
| html-entities | encode-as-text | 100% | 40% | 0% | 0% | 869,440 |
| he.escape | encode-as-text | 100% | 40% | 0% | 0% | 952,948 |
84 attack vectors (OWASP, PortSwigger, cure53 corpora) · 15 benign documents.
* This is an observed corpus result, not a security guarantee. † Throughput is
the median of seven warmed-up samples captured on 2026-08-02 with Node 26.3.0
on Apple Silicon. It varies by machine and is only meaningful within the same
category. In the strip-to-text rows, striptags is smaller and faster in this
snapshot; Purifai's distinction is exact benign text together with complete
raw-container body removal and bounded malformed-input scaling. The full
package build is about 4.5 KB gzipped, while a tree-shaken sanitize import is
3.5 KB minified / 1.6 KB gzipped. Reproduce the current results from the repository:
pnpm install
pnpm benchmark
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,
});
No DOM shim and no runtime dependencies. The result is plain text intended for normal framework text interpolation, not for a URL, CSS, or JavaScript context.
Design and testing
Purifai decodes syntax-relevant encodings, validates numeric entity scalars,
then consumes markup with a bounded forward scanner. Scriptable and raw-text
containers are removed together with their bodies; benign prose such as
London=Paris, 100%20off, and a<b && c>d is preserved as text.
The test suite combines curated attack vectors, browser re-parsing, seeded fuzzing, consumer examples, and adversarial 2–128 KiB inputs. Both top-level malformed tags and malformed candidates nested inside dropped containers have near-linear scaling regressions.
Idempotence matters for a delete-based sanitizer: running the output through the pipeline again must not reveal a new match or change the result. That invariant is tested alongside security and content retention.
Choosing strip-to-text
Purifai is useful when markup is not part of the product requirement. Search indexes, notification previews, profile summaries, audit logs, AI inputs, CSV exports, and compact cards usually need the words—not the original tags and attributes.
Removing markup makes the output contract easy to explain: text should survive; executable structure should not. That is different from a rich-text editor, where headings, links, tables, and inline emphasis must remain. For those surfaces, a mature preserve-HTML sanitizer is the better tool.
The distinction is important because the two categories should not be compared only by how many attack strings produce an empty result. A sanitizer that keeps safe formatting is doing more work than one that intentionally emits text.
Security boundaries
Sanitization is one layer, not an application security policy. Purifai does not decide who may create content, where that content may be stored, or which users may retrieve it. It also does not replace output encoding when a value moves into JavaScript, CSS, a URL, SQL, or another interpreter.
A strong deployment still combines validation, contextual encoding, a Content Security Policy where applicable, dependency review, and tests at the actual render boundary. Purifai narrows one specific problem: converting untrusted markup into text without requiring a DOM.
Operational use
The companion analyze() and isDangerous() APIs support logging and review
flows, while batch helpers cover imports and pipelines. Detection should inform
an operational decision rather than silently becoming one: a product may
sanitize ordinary content, quarantine repeated hostile input, or retain decoded
evidence for an abuse team.
Inputs should still have a practical size limit, and changes to sanitization rules should be rolled out against representative content. Security tests prove the dangerous structure is gone; fidelity tests prove the useful text did not disappear with it.
