purifai 2.0.1 β 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -100
- package/dist/index.cjs +206 -137
- package/dist/index.d.cts +18 -20
- package/dist/index.d.ts +18 -20
- package/dist/index.js +206 -137
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -9,49 +9,65 @@
|
|
|
9
9
|
|
|
10
10
|
Purifai is a lightweight, zero-dependency sanitizer for the **strip-to-text** case:
|
|
11
11
|
untrusted content that must be displayed as text, never as markup. It removes all
|
|
12
|
-
HTML rather than allow-listing safe tags
|
|
13
|
-
|
|
12
|
+
HTML rather than allow-listing safe tags. The result contains no retained markup
|
|
13
|
+
for an HTML parser to mutate on re-parse.
|
|
14
14
|
|
|
15
15
|
**Reach for Purifai when** you want untrusted input rendered as plain text, with no
|
|
16
|
-
dependencies, no DOM, and a ~4.
|
|
16
|
+
dependencies, no DOM, and a ~4.5 KB gzipped package build that runs in Node, browsers,
|
|
17
17
|
and edge runtimes alike.
|
|
18
18
|
|
|
19
19
|
**Reach for DOMPurify or sanitize-html when** you need to *keep* safe formatting
|
|
20
20
|
such as `<b>` and `<a href>`. That is a harder problem and they solve it well.
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Benchmark snapshot
|
|
23
23
|
|
|
24
|
-
Produced by `pnpm
|
|
24
|
+
Produced by `pnpm benchmark`. Each tool's output is inserted into a real DOM
|
|
25
25
|
(jsdom), then serialized and re-parsed β the round trip where mutation XSS lives.
|
|
26
|
-
A vector counts as
|
|
27
|
-
handler, or a dangerous-protocol URL.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
|
32
|
-
|
|
33
|
-
| sanitize
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
26
|
+
A vector counts as having no executable output only if *neither* parse yields a
|
|
27
|
+
script node, an `on*` handler, or a dangerous-protocol URL. Exact-text fidelity
|
|
28
|
+
and raw-container removal are separate measurements so deletion cannot masquerade
|
|
29
|
+
as quality.
|
|
30
|
+
|
|
31
|
+
| Library | Category | No executable output* | Exact text | Markup kept | Raw body removed | Median ops/secβ | p95 Β΅s/opβ |
|
|
32
|
+
|---------|----------|----------------------|------------|-------------|------------------|----------------|------------|
|
|
33
|
+
| **Purifai.sanitize** | strip-to-text | **100%** | **100%** | 0% *(by design)* | **100%** | **526,709** | **2.050** |
|
|
34
|
+
| Purifai.escape | encode-as-text | 100% | 40% | 0% | 0% | 596,421 | 3.627 |
|
|
35
|
+
| striptags | strip-to-text | 100% | 100% | 0% | 20% | 633,697 | 1.836 |
|
|
36
|
+
| DOMPurify (jsdom) | preserve-safe-html | 100% | 100% | 100% | 60% | 1,686 | 698.853 |
|
|
37
|
+
| sanitize-html | preserve-safe-html | 100% | 100% | 100% | 60% | 88,492 | 14.731 |
|
|
38
|
+
| xss | preserve-safe-html | 100% | 100% | 100% | 0% | 335,448 | 4.079 |
|
|
39
|
+
| rehype-sanitize | preserve-safe-html | 100% | 100% | 100% | 40% | 24,522 | 44.439 |
|
|
40
|
+
| escape-html | encode-as-text | 100% | 40% | 0% | 0% | 2,238,804 | 0.452 |
|
|
41
|
+
| validator.escape | encode-as-text | 100% | 40% | 0% | 0% | 735,565 | 1.494 |
|
|
42
|
+
| entities.escapeUTF8 | encode-as-text | 100% | 40% | 0% | 0% | 1,208,824 | 1.453 |
|
|
43
|
+
| html-entities | encode-as-text | 100% | 40% | 0% | 0% | 869,440 | 1.203 |
|
|
44
|
+
| he.escape | encode-as-text | 100% | 40% | 0% | 0% | 952,948 | 1.242 |
|
|
37
45
|
|
|
38
46
|
84 attack vectors (OWASP, PortSwigger, cure53 corpora) Β· 15 benign documents.
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
\* This is an observed corpus result, not a security guarantee. β Throughput is
|
|
49
|
+
the median of seven samples after warm-up; p95 is the slowest of those seven
|
|
50
|
+
local samples. Snapshot captured on 2026-08-02
|
|
51
|
+
with Node 26.3.0 on Apple Silicon. It varies by hardware and runtime and should
|
|
52
|
+
only be compared within the same category. The command above is the source of
|
|
53
|
+
truth. HTML encoders safely display the original markup as text; unlike Purifai
|
|
54
|
+
and striptags, they do not turn markup-bearing input into clean reader text.
|
|
55
|
+
|
|
56
|
+
Purifai is not a drop-in replacement for a safe-HTML-preserving sanitizer. Its
|
|
57
|
+
measured advantages are a bounded scanner, zero runtime dependencies, and no DOM
|
|
58
|
+
requirement for the narrower strip-to-text job.
|
|
59
|
+
|
|
60
|
+
Within the strip-to-text rows, striptags is smaller and faster in this snapshot;
|
|
61
|
+
Purifai's differentiation is exact benign text together with full raw-container
|
|
62
|
+
body removal and bounded malformed-input scaling.
|
|
47
63
|
|
|
48
64
|
## π Why Purifai?
|
|
49
65
|
|
|
50
|
-
### β
Security
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
66
|
+
### β
Security boundaries
|
|
67
|
+
- Produces no executable output in the current 84-vector DOM re-parse corpus
|
|
68
|
+
- Uses a forward-only scanner with measured near-linear adversarial scaling
|
|
69
|
+
- Removes scriptable/raw-text containers together with their bodies
|
|
70
|
+
- Rejects invalid numeric entity scalars instead of emitting control characters
|
|
55
71
|
- Contextual encoders for HTML body, attribute, and URL contexts
|
|
56
72
|
|
|
57
73
|
### β‘ High Performance
|
|
@@ -66,31 +82,12 @@ handler, or a dangerous-protocol URL.
|
|
|
66
82
|
- Batch processing support
|
|
67
83
|
- Comprehensive documentation
|
|
68
84
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
Purifai blocks **all 84 vectors** in the corpus (64 classic + 20 modern mXSS/namespace), including:
|
|
72
|
-
|
|
73
|
-
### Critical Polyglot Attacks
|
|
74
|
-
```javascript
|
|
75
|
-
// β
Purifai reduces each of these to empty output:
|
|
76
|
-
|
|
77
|
-
// Universal XSS Polyglot
|
|
78
|
-
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()///>\\x3e
|
|
79
|
-
|
|
80
|
-
// Ultimate XSS Polyglot
|
|
81
|
-
javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/[*/[]/+alert(1)//'>'
|
|
85
|
+
## Test coverage
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
### Standard XSS Vectors
|
|
88
|
-
- Script injection: `<script>alert("xss")</script>`
|
|
89
|
-
- Event handlers: `<img src=x onerror=alert(1)>`
|
|
90
|
-
- Protocol injection: `javascript:alert(1)`
|
|
91
|
-
- CSS expressions: `<div style="expression(alert(1))">`
|
|
92
|
-
- Template injection: `{{constructor.constructor("alert(1)")()}}`
|
|
93
|
-
- Encoding bypasses: `<script>alert(1)</script>`
|
|
87
|
+
The repository exercises 64 classic and 20 modern mutation/namespace vectors,
|
|
88
|
+
seeded fuzzing, real-browser parsing, malformed raw-text containers, URL context
|
|
89
|
+
validation, idempotence, and adversarial inputs from 2β128 KiB. A passing corpus
|
|
90
|
+
is regression evidence, not proof against every future browser or payload.
|
|
94
91
|
|
|
95
92
|
## π οΈ Installation
|
|
96
93
|
|
|
@@ -115,8 +112,7 @@ console.log(clean); // "Hello World"
|
|
|
115
112
|
|
|
116
113
|
// With options
|
|
117
114
|
const safe = Purifai.sanitize(userInput, {
|
|
118
|
-
maxLength: 10000
|
|
119
|
-
aggressiveMode: true
|
|
115
|
+
maxLength: 10000
|
|
120
116
|
});
|
|
121
117
|
```
|
|
122
118
|
|
|
@@ -154,8 +150,9 @@ const cleanData = sanitizeBatch(userInputs);
|
|
|
154
150
|
import { isDangerous } from 'purifai';
|
|
155
151
|
|
|
156
152
|
if (isDangerous(userInput)) {
|
|
157
|
-
//
|
|
158
|
-
|
|
153
|
+
// Optional telemetry only. Do not use this advisory signal as an
|
|
154
|
+
// authorization, authentication, or request-blocking decision.
|
|
155
|
+
console.warn('Potentially dangerous markup observed');
|
|
159
156
|
}
|
|
160
157
|
```
|
|
161
158
|
|
|
@@ -182,10 +179,9 @@ escapeUrl('javascript:alert(1)'); // ""
|
|
|
182
179
|
escapeUrl('java\tscript:alert(1)'); // "" (whitespace-split protocols too)
|
|
183
180
|
```
|
|
184
181
|
|
|
185
|
-
Why both exist: `
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
guesses.
|
|
182
|
+
Why both exist: `sanitize()` uses a conservative HTML-like scanner and returns
|
|
183
|
+
clean reader text, while `escape()` is lossless for text that must be displayed
|
|
184
|
+
exactly. Only the caller knows the destination context.
|
|
189
185
|
|
|
190
186
|
## βοΈ Configuration Options
|
|
191
187
|
|
|
@@ -194,19 +190,49 @@ interface PurifaiOptions {
|
|
|
194
190
|
/** Maximum input length (default: 1MB) */
|
|
195
191
|
maxLength?: number;
|
|
196
192
|
|
|
197
|
-
/**
|
|
193
|
+
/** Subset of built-in safe protocols: http, https, mailto */
|
|
198
194
|
allowedProtocols?: string[];
|
|
199
195
|
|
|
200
|
-
/**
|
|
196
|
+
/** @deprecated Retained for compatibility; strip-to-text is always used. */
|
|
201
197
|
aggressiveMode?: boolean;
|
|
202
198
|
}
|
|
203
199
|
```
|
|
204
200
|
|
|
201
|
+
`allowedProtocols` can narrow the built-in set but cannot add executable
|
|
202
|
+
schemes. Protocol-relative URLs are rejected.
|
|
203
|
+
|
|
204
|
+
## Test Purifai in your project
|
|
205
|
+
|
|
206
|
+
The example below imports only the public package API and runs with Node's
|
|
207
|
+
built-in test runner:
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
import test from 'node:test';
|
|
211
|
+
import assert from 'node:assert/strict';
|
|
212
|
+
import { sanitize, escape, escapeUrl } from 'purifai';
|
|
213
|
+
|
|
214
|
+
test('renders untrusted markup as plain text', () => {
|
|
215
|
+
assert.equal(sanitize('<script>bad()</script><b>Hello</b>'), 'Hello');
|
|
216
|
+
assert.equal(escape('a<b && c>d'), 'a<b && c>d');
|
|
217
|
+
assert.equal(escapeUrl('javascript:alert(1)'), '');
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Save it as `purifai.test.mjs`, then run:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
node --test purifai.test.mjs
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
This is a regression example, not proof that an application is secure. Keep
|
|
228
|
+
authorization separate and use the encoder for the actual output context. The
|
|
229
|
+
repository-owned copy runs with `pnpm test:example`.
|
|
230
|
+
|
|
205
231
|
## π§ͺ Testing Methodology
|
|
206
232
|
|
|
207
233
|
Our comprehensive test suite evaluates sanitizers against:
|
|
208
234
|
|
|
209
|
-
- **
|
|
235
|
+
- **84 attack vectors** from OWASP, PortSwigger, cure53, and regression research
|
|
210
236
|
- **Advanced polyglot attacks** that combine multiple bypass techniques
|
|
211
237
|
- **Encoding variations** (Unicode, HTML entities, URL encoding)
|
|
212
238
|
- **Context-breaking attacks** for different HTML contexts
|
|
@@ -234,14 +260,26 @@ behaviour as failure. Run `pnpm test:fair` for numbers that survive scrutiny.
|
|
|
234
260
|
|
|
235
261
|
### Bundle Size Comparison
|
|
236
262
|
|
|
237
|
-
| Library |
|
|
238
|
-
|
|
239
|
-
| **Purifai** |
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
| sanitize-html |
|
|
263
|
+
| Library | Category | Target | Minified | Gzip | Direct runtime deps |
|
|
264
|
+
|---------|----------|--------|----------|------|---------------------|
|
|
265
|
+
| **Purifai** | strip-text | browser | **3.5 KB** | **1.6 KB** | **0** |
|
|
266
|
+
| striptags | strip-text | browser | 2.1 KB | 1.1 KB | 0 |
|
|
267
|
+
| DOMPurify | preserve-html | browser | 28.0 KB | 10.6 KB | 0 |
|
|
268
|
+
| sanitize-html | preserve-html | Node | 192.2 KB | 70.4 KB | 7 |
|
|
269
|
+
| xss | preserve-html | browser | 18.4 KB | 6.2 KB | 2 |
|
|
270
|
+
| rehype-sanitize | preserve-html | browser | 244.5 KB | 70.7 KB | 2 |
|
|
271
|
+
| escape-html | escape-html | browser | 1.2 KB | 0.7 KB | 0 |
|
|
272
|
+
| validator.escape | escape-html | browser | 0.4 KB | 0.2 KB | 0 |
|
|
273
|
+
| entities.escapeUTF8 | escape-html | browser | 0.7 KB | 0.4 KB | 0 |
|
|
274
|
+
| html-entities | escape-html | browser | 34.8 KB | 13.1 KB | 0 |
|
|
275
|
+
| he.escape | escape-html | browser | 85.7 KB | 30.2 KB | 0 |
|
|
276
|
+
|
|
277
|
+
Measured by `pnpm test:size` with esbuild 0.27.7: smallest supported ESM
|
|
278
|
+
import, bundled and minified for ES2020, then gzipped. The lockfile pins the
|
|
279
|
+
exact library versions. DOMPurify is measured against the native browser API;
|
|
280
|
+
sanitize-html is a Node bundle; the rehype row includes the parser, sanitizer,
|
|
281
|
+
and serializer pipeline. Direct dependency counts come from each named package's
|
|
282
|
+
manifest. Compare sizes within a category and targetβthe tools do different jobs.
|
|
245
283
|
|
|
246
284
|
## π Use Cases
|
|
247
285
|
|
|
@@ -264,21 +302,12 @@ const cleanText = Purifai.sanitize(editorContent, {
|
|
|
264
302
|
});
|
|
265
303
|
```
|
|
266
304
|
|
|
267
|
-
### API Gateways
|
|
268
|
-
```typescript
|
|
269
|
-
// Sanitize all incoming string data
|
|
270
|
-
const sanitizedPayload = sanitizeBatch(Object.values(request.body));
|
|
271
|
-
```
|
|
272
|
-
|
|
273
305
|
### Real-time Chat
|
|
274
306
|
```typescript
|
|
275
307
|
// Clean messages before broadcasting
|
|
276
308
|
socket.on('message', (data) => {
|
|
277
309
|
const result = analyze(data.message);
|
|
278
|
-
|
|
279
|
-
// Block and log the attempt
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
310
|
+
// hadThreats/threatLevel are advisory telemetry, not an auth gate.
|
|
282
311
|
broadcast(result.content);
|
|
283
312
|
});
|
|
284
313
|
```
|
|
@@ -294,6 +323,7 @@ const clean = DOMPurify.sanitize(dirty);
|
|
|
294
323
|
// After
|
|
295
324
|
import { sanitize } from 'purifai';
|
|
296
325
|
const clean = sanitize(dirty);
|
|
326
|
+
// Only migrate when dropping every tag is intended. Otherwise keep DOMPurify.
|
|
297
327
|
```
|
|
298
328
|
|
|
299
329
|
### From sanitize-html
|
|
@@ -318,6 +348,7 @@ const clean = xss(dirty);
|
|
|
318
348
|
// After
|
|
319
349
|
import { sanitize } from 'purifai';
|
|
320
350
|
const clean = sanitize(dirty);
|
|
351
|
+
// Only migrate when dropping every tag is intended. Otherwise keep xss.
|
|
321
352
|
```
|
|
322
353
|
|
|
323
354
|
### From validator.js
|
|
@@ -326,9 +357,9 @@ const clean = sanitize(dirty);
|
|
|
326
357
|
import validator from 'validator';
|
|
327
358
|
const clean = validator.escape(dirty);
|
|
328
359
|
|
|
329
|
-
// After
|
|
330
|
-
import {
|
|
331
|
-
const clean =
|
|
360
|
+
// After: preserve the original encode-as-text behavior
|
|
361
|
+
import { escape } from 'purifai';
|
|
362
|
+
const clean = escape(dirty);
|
|
332
363
|
```
|
|
333
364
|
|
|
334
365
|
### From node-sanitize
|
|
@@ -344,15 +375,15 @@ const clean = sanitize(dirty);
|
|
|
344
375
|
|
|
345
376
|
## π Security Features
|
|
346
377
|
|
|
347
|
-
###
|
|
348
|
-
- **
|
|
349
|
-
- **
|
|
350
|
-
- **
|
|
351
|
-
- **
|
|
378
|
+
### Defensive design
|
|
379
|
+
- **Forward-only scanning** with bounded adversarial scaling checks
|
|
380
|
+
- **Fail-closed raw-text removal** for unclosed scriptable containers
|
|
381
|
+
- **Context-specific encoders** instead of one output reused everywhere
|
|
382
|
+
- **No executable output observed** across the current 84-vector corpus
|
|
352
383
|
|
|
353
384
|
### Encoded Attack Detection
|
|
354
385
|
```typescript
|
|
355
|
-
//
|
|
386
|
+
// Markup variants are decoded before the strip-to-text scan:
|
|
356
387
|
'<script>alert(1)</script>' // Direct
|
|
357
388
|
'<script>alert(1)</script>' // HTML entities
|
|
358
389
|
'%3Cscript%3Ealert(1)%3C/script%3E' // URL encoded
|
|
@@ -361,12 +392,10 @@ const clean = sanitize(dirty);
|
|
|
361
392
|
|
|
362
393
|
## π Performance Optimization
|
|
363
394
|
|
|
364
|
-
|
|
365
|
-
-
|
|
366
|
-
|
|
367
|
-
-
|
|
368
|
-
- **Minimal CPU** usage through intelligent algorithms
|
|
369
|
-
- **Scalable** performance across different input sizes and complexity
|
|
395
|
+
`pnpm test:perf` measures two malformed-tag shapes from 2β128 KiB. In the
|
|
396
|
+
2026-08-02 Node 26.3.0 run, 64Γ larger inputs took 67.55Γ and 59.88Γ longer;
|
|
397
|
+
normalized cost stayed at 1.06Γ and 0.94Γ. These local results support the
|
|
398
|
+
scanner's near-linear design but are not a universal runtime guarantee.
|
|
370
399
|
|
|
371
400
|
## π€ Contributing
|
|
372
401
|
|
|
@@ -374,7 +403,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
374
403
|
|
|
375
404
|
### Development Setup
|
|
376
405
|
```bash
|
|
377
|
-
git clone https://github.com/
|
|
406
|
+
git clone https://github.com/moji2002/purifai.git
|
|
378
407
|
cd purifai
|
|
379
408
|
pnpm install
|
|
380
409
|
pnpm build
|
|
@@ -383,9 +412,14 @@ pnpm test
|
|
|
383
412
|
|
|
384
413
|
### Running Benchmarks
|
|
385
414
|
```bash
|
|
386
|
-
pnpm benchmark
|
|
415
|
+
pnpm benchmark
|
|
387
416
|
```
|
|
388
417
|
|
|
418
|
+
This builds the published artifact, runs the two-axis competitor benchmark, and
|
|
419
|
+
then measures Purifai's throughput, critical-attack checks, and bundle size. The
|
|
420
|
+
competitor set and exact versions are pinned in `package.json` and
|
|
421
|
+
`pnpm-lock.yaml` so results are reproducible.
|
|
422
|
+
|
|
389
423
|
## π License
|
|
390
424
|
|
|
391
425
|
MIT License - see [LICENSE](LICENSE) file for details.
|
|
@@ -404,10 +438,10 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
404
438
|
|
|
405
439
|
---
|
|
406
440
|
|
|
407
|
-
**β‘
|
|
441
|
+
**β‘ Need untrusted markup reduced to plain text? Install Purifai.**
|
|
408
442
|
|
|
409
443
|
```bash
|
|
410
444
|
npm install purifai
|
|
411
445
|
```
|
|
412
446
|
|
|
413
|
-
*Purifai
|
|
447
|
+
*Purifai β strip untrusted markup, keep the text.*
|