Introducing Purifai: A Zero-Dependency Strip-to-Text Sanitizer
Cross-site scripting (XSS) is still one of the top risks on the OWASP Top 10. Every time you render user-generated content—comments, rich text, profile bios—you're opening a door. The standard fix is sanitization: strip or escape dangerous HTML before it hits the DOM. There are two different jobs hiding behind that one word. One is preserving safe HTML while removing the dangerous parts — what DOMPurify and sanitize-html do, and the harder problem. The other is emitting untrusted content as plain text, where nothing needs to survive. I built Purifai for the second job, in a footprint small enough for edge runtimes with no DOM available.
Purifai is a zero-dependency strip-to-text sanitizer: it removes markup rather than allow-listing safe tags. That leaves no markup for a parser to mutate on re-parse, and it places Purifai in a different category from DOMPurify, which exists to keep safe formatting. It's TypeScript-native, 4.1 KB gzipped, needs no DOM, and runs in Node, browsers, edge runtimes, and workers.

Quick Start
Install it:
npm install purifai # or pnpm add purifai
Use it:
import { Purifai } from 'purifai'; // Simple sanitization const clean = Purifai.sanitize('<script>alert("xss")</script>Hello World'); console.log(clean); // "Hello World" // With options for stricter control const safe = Purifai.sanitize(userInput, { maxLength: 10000, allowBasicHtml: false, aggressiveMode: true });
That's it. No DOM, no external dependencies. Just a function that takes dirty HTML and returns safe output.
Security and Content Fidelity
The benchmark inserts each sanitizer's output into a real DOM (jsdom),
serializes and re-parses it, and counts a vector blocked only if neither parse
yields a script node, an on* handler, or a dangerous-protocol URL. It reports
content fidelity alongside security:
| 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, including the mutation-XSS and namespace-confusion corpora from cure53 and PortSwigger.
Every maintained sanitizer in the comparison blocks the full suite. Purifai's 0% markup retention is its design: it exists for content that should be displayed as text, with no surviving markup for a browser to reinterpret.
Where Purifai Fits
DOMPurify and sanitize-html preserve safe markup, which is the right choice for rich text. Purifai is designed for places where the output should be text: labels, previews, notifications, search indexes, logs, and API pipelines. Removing markup also means it can run without a browser DOM or a server-side DOM shim.
The choice is about output requirements. Keep a preserve-HTML sanitizer when formatting must survive; use Purifai when plain text is the intended result.
How Purifai Approaches the Problem
Purifai uses a multi-stage pipeline without relying on a runtime DOM. It:
- Normalizes encodings first — Decodes HTML entities, URL encoding, and Unicode escapes before applying rules.
- Handles context switches — Tracks when the parser moves between HTML, SVG, script, and style contexts.
- Blocks protocol injection —
javascript:,vbscript:,data:URIs are stripped from attributes. - Offers aggressive mode —
aggressiveMode: true(default) applies stricter rules and fallback checks.
aggressiveMode is the right choice for user-generated content, chat systems, CMS content, or anywhere you can't fully trust the input. Turn it off only if you have a controlled input source and need to preserve more HTML structure.
Key Features
Zero dependencies. No DOM, no jsdom, no cheerio. Minimal bundle size (~12KB) and a tiny attack surface. Works in Node and the browser.
Threat analysis. Use analyze() when you need more than sanitization—logging, blocking, or incident response:
import { analyze } from 'purifai'; const result = analyze('<script>alert("hack")</script>User content'); console.log(result.content); // "User content" console.log(result.hadThreats); // true console.log(result.threatLevel); // "critical" if (result.hadThreats) { console.warn('Potential XSS detected', { level: result.threatLevel }); } broadcast(result.content); // Safe to use
Batch processing. Sanitize multiple strings at once for APIs or content pipelines:
import { sanitizeBatch } from 'purifai'; const cleanData = sanitizeBatch([ '<script>alert(1)</script>Hello', '<img src=x onerror=alert(1)>World', 'Safe content' ]); // ["Hello", "World", "Safe content"]
Danger check. Quick pre-scan with isDangerous() for logging or blocking before full sanitization.
Choosing Purifai over a Preserve-HTML Sanitizer
Purifai is not a drop-in replacement when your product needs to retain rich-text formatting. Its API is straightforward to adopt when the desired output is plain text or a small, explicitly enabled set of basic tags.
When Migration Makes Sense
- Plain-text output — Markup does not need to survive
- Edge and worker runtimes — No browser DOM or jsdom available
- Small dependency budget — A compact, zero-dependency package
- TypeScript projects — Typed APIs without separate declarations
From DOMPurify
Before:
import DOMPurify from 'dompurify'; const clean = DOMPurify.sanitize(dirty);
After:
import { sanitize } from 'purifai'; const clean = sanitize(dirty);
If you were using DOMPurify in Node with jsdom, you no longer need jsdom—Purifai doesn't use the DOM.
From sanitize-html
Before:
import sanitizeHtml from 'sanitize-html'; const clean = sanitizeHtml(dirty, { allowedTags: ['b', 'i', 'em', 'strong', 'p'], allowedAttributes: { a: ['href'] }, });
After:
import { sanitize } from 'purifai'; const clean = sanitize(dirty, { allowBasicHtml: true, maxLength: 50000, allowedProtocols: ['http', 'https', 'mailto'], aggressiveMode: true });
allowBasicHtml: true enables a curated set of safe inline tags. For "strip everything," use allowBasicHtml: false (default).
Edge Cases
Custom protocols. Purifai defaults to http, https, and mailto. For others (e.g. tel:):
sanitize(dirty, { allowedProtocols: ['http', 'https', 'mailto', 'tel'] });
Max length. Cap input size to avoid DoS:
sanitize(dirty, { maxLength: 10000 });
Batch processing. For many strings (e.g. API request bodies):
import { sanitizeBatch } from 'purifai'; const cleanData = sanitizeBatch(Object.values(request.body));
Testing Strategy
Don't switch cold. Run both sanitizers in parallel during rollout:
- Install Purifai alongside your current library.
- Add a comparison layer in development or staging:
import { sanitize as purifaiSanitize } from 'purifai'; import DOMPurify from 'dompurify'; function sanitizeWithComparison(input: string) { const purifaiResult = purifaiSanitize(input); const dompurifyResult = DOMPurify.sanitize(input); if (purifaiResult !== dompurifyResult) { console.warn('Sanitizer output differs', { input, purifaiResult, dompurifyResult }); } return purifaiResult; }
- Log differences—Purifai may strip more because its default output is plain text.
- Run your test suite. Fix any legitimate content that gets over-stripped.
- Deploy, then remove the old library and comparison code.
Quick Checklist
- Install:
pnpm add purifai - Replace imports:
sanitizefrompurifai - Map options:
allowBasicHtml,maxLength,allowedProtocolsas needed - Remove jsdom (if you only had it for DOMPurify)
- Run both sanitizers in parallel in staging
- Run tests, fix any over-stripping
- Deploy and remove old dependency
Wrap-up
Purifai offers a compact option for turning untrusted markup into text without a DOM dependency. If your product needs rich text, keep a sanitizer built to preserve safe HTML; if it needs text, Purifai keeps that path small and portable.
Purifai is on npm and open source on GitHub. Run the benchmark yourself with pnpm benchmark; the suite compares security and content fidelity across Purifai, DOMPurify, sanitize-html, and xss. The README and GitHub issues cover usage and edge cases.
