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 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, which makes it immune to mutation XSS by
13
- construction β€” nothing survives for a parser to mutate on re-parse.
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.1 KB gzipped footprint that runs in Node, browsers,
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
- ## πŸ† Benchmark Results
22
+ ## Benchmark snapshot
23
23
 
24
- Produced by `pnpm test:fair`. Each sanitizer's output is inserted into a real DOM
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 blocked only if *neither* parse yields a script node, an `on*`
27
- handler, or a dangerous-protocol URL.
28
-
29
- | Library | Category | Security | Text kept | Markup kept |
30
- |---------|----------|----------|-----------|-------------|
31
- | **Purifai** | strip-to-text | **100%** | **100%** | 0% *(by design)* |
32
- | DOMPurify | preserve-html | 100% | 100% | 100% |
33
- | sanitize-html | preserve-html | 100% | 100% | 100% |
34
- | xss | preserve-html | 100% | 100% | 100% |
35
- | `() => ''` *(calibration)* | β€” | 100% | 0% | 0% |
36
- | `v => v` *(calibration)* | β€” | 38.1% | 100% | 100% |
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
- > πŸ“Š **Read this honestly.** On security there is no headroom left β€” every
41
- > maintained sanitizer above blocks everything thrown at it. The calibration rows
42
- > are the whole point: a function returning `''` also scores 100% security, which
43
- > is why a security number means nothing without a fidelity axis beside it.
44
- > Purifai's 0% markup retention is its design, not a defect β€” but it does mean
45
- > Purifai is **not** "more secure than DOMPurify". It is smaller, dependency-free,
46
- > and needs no DOM.
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
- - Blocks every vector in the 84-payload corpus, verified by real DOM re-parse
52
- - Handles **Unicode**, **HTML entity**, and **URL encoding** bypasses
53
- - Prevents **template injection** and **CSS expression** attacks
54
- - Immune to **mutation XSS** by construction: it emits no markup to mutate
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
- ## πŸ”₯ Attack Vectors Blocked
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
- // Namespace Confusion Attack
84
- <form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>
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: `&#60;script&#62;alert(1)&#60;/script&#62;`
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
- // Log security incident
158
- console.warn('Potential XSS attack detected');
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: `a<b && c>d` is a valid HTML start tag per the parsing spec, so
186
- `sanitize()` correctly drops it while `escape()` preserves it verbatim. Only the
187
- caller knows whether a string is markup or text β€” so the API asks rather than
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
- /** Custom allowed protocols (default: ['http', 'https', 'mailto']) */
193
+ /** Subset of built-in safe protocols: http, https, mailto */
198
194
  allowedProtocols?: string[];
199
195
 
200
- /** Enable aggressive mode for maximum security (default: true) */
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&lt;b &amp;&amp; c&gt;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
- - **64 sophisticated attack vectors** from OWASP, PortSwigger, and security research
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 | Bundle Size | Dependencies | TypeScript |
238
- |---------|-------------|--------------|-------------|
239
- | **Purifai** | **13.6KB / 4.1KB gzip** | **0** | **βœ… Native** |
240
- | validator.js | ~15KB | 0 | βœ… Available |
241
- | xss | ~25KB | 3 | ❌ None |
242
- | node-sanitize | ~32KB | 5 | ❌ None |
243
- | DOMPurify | ~45KB | 0 | βœ… Available |
244
- | sanitize-html | ~200KB+ | 15+ | βœ… Available |
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
- if (result.threatLevel === 'critical') {
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 { sanitize } from 'purifai';
331
- const clean = sanitize(dirty);
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
- ### Advanced Protection Techniques
348
- - **Multi-layer sanitization** with fallback mechanisms
349
- - **Context-aware parsing** to prevent bypass attempts
350
- - **Aggressive mode** for maximum security applications
351
- - **Zero false negatives** in comprehensive testing
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
- // All these variants are detected and blocked:
386
+ // Markup variants are decoded before the strip-to-text scan:
356
387
  '<script>alert(1)</script>' // Direct
357
388
  '&#60;script&#62;alert(1)&#60;/script&#62;' // 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
- Purifai is optimized for:
365
- - **High-throughput** applications with efficient processing
366
- - **Low memory** footprint with optimized regex patterns
367
- - **Fast startup** with zero dependencies
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/purifai/purifai.git
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 # Compare against other libraries
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
- **⚑ Ready to secure your application? Install Purifai today and join the ranks of applications with bulletproof XSS protection.**
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 - Because your users' security shouldn't be compromised.*
447
+ *Purifai β€” strip untrusted markup, keep the text.*