purifai 1.0.0 โ†’ 2.0.1

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
@@ -5,44 +5,54 @@
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
6
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-green.svg)](https://www.npmjs.com/package/purifai)
7
7
 
8
- > **Ultra-Secure HTML Sanitizer with Advanced XSS Protection**
8
+ > **Zero-dependency strip-to-text sanitizer with contextual output encoding**
9
9
 
10
- Purifai is a lightweight, zero-dependency HTML sanitizer that provides **industry-leading XSS protection**. In comprehensive testing against 64 sophisticated attack vectors, Purifai achieved a **100% security success rate** - the only library to block all attacks including advanced polyglot vectors that bypass other popular sanitizers.
10
+ Purifai is a lightweight, zero-dependency sanitizer for the **strip-to-text** case:
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.
11
14
 
12
- ## ๐Ÿ† Benchmark Results
13
-
14
- ### Security Performance (64 Attack Vectors)
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,
17
+ and edge runtimes alike.
15
18
 
16
- | Library | Success Rate | Blocked | Failed | Performance Rank |
17
- |---------|-------------|---------|---------|------------------|
18
- | **๐Ÿฅ‡ Purifai** | **100.0%** | **64/64** | **0** | **#1 Security** |
19
- | ๐Ÿฅˆ sanitize-html | 79.7% | 51/64 | 13 | #2 Security |
20
- | ๐Ÿฅ‰ DOMPurify | 62.5% | 40/64 | 24 | #3 Security |
21
- | node-sanitize | 56.3% | 36/64 | 28 | #4 Security |
22
- | xss | 20.3% | 13/64 | 51 | #5 Security |
23
- | validator.js | 7.8% | 5/64 | 59 | #6 Security |
19
+ **Reach for DOMPurify or sanitize-html when** you need to *keep* safe formatting
20
+ such as `<b>` and `<a href>`. That is a harder problem and they solve it well.
24
21
 
25
- ### Performance Profile
26
-
27
- | Library | Performance | Security Trade-off | Overall Score |
28
- |---------|-------------|-------------------|---------------|
29
- | **Purifai** | **โšก Fast** | **๐Ÿ›ก๏ธ Perfect (100%)** | **๐Ÿ† Excellent** |
30
- | sanitize-html | โšก Moderate | ๐Ÿ›ก๏ธ Good (79.7%) | โœ… Good |
31
- | DOMPurify | ๐ŸŒ Slow | ๐Ÿ›ก๏ธ Fair (62.5%) | โš ๏ธ Fair |
32
- | xss | โšก Fast | โŒ Poor (20.3%) | โŒ Poor |
33
- | validator.js | โšก Very Fast | โŒ Very Poor (7.8%) | โŒ Unacceptable |
34
- | node-sanitize | โšก Fast | ๐Ÿ›ก๏ธ Fair (56.3%) | โš ๏ธ Fair |
22
+ ## ๐Ÿ† Benchmark Results
35
23
 
36
- > ๐Ÿ“Š **Key Insight**: Purifai achieves **perfect security (100% success rate)** - significantly better than sanitize-html (79.7%) and DOMPurify (62.5%), while maintaining fast performance comparable to leading libraries.
24
+ Produced by `pnpm test:fair`. Each sanitizer's output is inserted into a real DOM
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% |
37
+
38
+ 84 attack vectors (OWASP, PortSwigger, cure53 corpora) ยท 15 benign documents.
39
+
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.
37
47
 
38
48
  ## ๐Ÿš€ Why Purifai?
39
49
 
40
- ### โœ… Unmatched Security
41
- - **100% success rate** against all tested attack vectors
42
- - Blocks advanced **polyglot attacks** that bypass other sanitizers
50
+ ### โœ… Security
51
+ - Blocks every vector in the 84-payload corpus, verified by real DOM re-parse
43
52
  - Handles **Unicode**, **HTML entity**, and **URL encoding** bypasses
44
53
  - Prevents **template injection** and **CSS expression** attacks
45
- - Mitigates **namespace confusion** and **DOM clobbering** techniques
54
+ - Immune to **mutation XSS** by construction: it emits no markup to mutate
55
+ - Contextual encoders for HTML body, attribute, and URL contexts
46
56
 
47
57
  ### โšก High Performance
48
58
  - **Fast processing** - optimized algorithms for high-throughput applications
@@ -58,11 +68,11 @@ Purifai is a lightweight, zero-dependency HTML sanitizer that provides **industr
58
68
 
59
69
  ## ๐Ÿ”ฅ Attack Vectors Blocked
60
70
 
61
- Purifai successfully blocks **all 64 tested attack vectors**, including:
71
+ Purifai blocks **all 84 vectors** in the corpus (64 classic + 20 modern mXSS/namespace), including:
62
72
 
63
- ### Critical Polyglot Attacks (Others Failed)
73
+ ### Critical Polyglot Attacks
64
74
  ```javascript
65
- // โœ… Purifai blocks these advanced attacks that bypass other sanitizers:
75
+ // โœ… Purifai reduces each of these to empty output:
66
76
 
67
77
  // Universal XSS Polyglot
68
78
  jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()///>\\x3e
@@ -106,7 +116,6 @@ console.log(clean); // "Hello World"
106
116
  // With options
107
117
  const safe = Purifai.sanitize(userInput, {
108
118
  maxLength: 10000,
109
- allowBasicHtml: false,
110
119
  aggressiveMode: true
111
120
  });
112
121
  ```
@@ -150,6 +159,34 @@ if (isDangerous(userInput)) {
150
159
  }
151
160
  ```
152
161
 
162
+ ### Contextual Escaping
163
+
164
+ `sanitize()` treats its input as HTML and removes markup. When the input is
165
+ **plain text**, escaping is the better tool โ€” it is lossless, and no guessing is
166
+ involved. These follow OWASP's context-specific output encoding guidance.
167
+
168
+ ```typescript
169
+ import { escape, escapeAttribute, escapeUrl } from 'purifai';
170
+
171
+ // HTML body context โ€” lossless, nothing is removed
172
+ escape('if (a<b && c>d) return;');
173
+ // "if (a&lt;b &amp;&amp; c&gt;d) return;"
174
+
175
+ // Attribute context โ€” safe even unquoted, since a bare space or
176
+ // backtick could otherwise close the value and start onerror=
177
+ `<div title="${escapeAttribute(userInput)}">`;
178
+
179
+ // URL context โ€” returns '' when the protocol is not allow-listed
180
+ escapeUrl('https://example.com'); // escaped, safe for href
181
+ escapeUrl('javascript:alert(1)'); // ""
182
+ escapeUrl('java\tscript:alert(1)'); // "" (whitespace-split protocols too)
183
+ ```
184
+
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.
189
+
153
190
  ## โš™๏ธ Configuration Options
154
191
 
155
192
  ```typescript
@@ -157,9 +194,6 @@ interface PurifaiOptions {
157
194
  /** Maximum input length (default: 1MB) */
158
195
  maxLength?: number;
159
196
 
160
- /** Allow safe HTML tags like <b>, <i>, <p> (default: false) */
161
- allowBasicHtml?: boolean;
162
-
163
197
  /** Custom allowed protocols (default: ['http', 'https', 'mailto']) */
164
198
  allowedProtocols?: string[];
165
199
 
@@ -193,19 +227,16 @@ Our comprehensive test suite evaluates sanitizers against:
193
227
 
194
228
  ### Security Comparison by Attack Type
195
229
 
196
- | Attack Category | Purifai | sanitize-html | DOMPurify | node-sanitize | xss | validator.js |
197
- |----------------|---------|---------------|-----------|---------------|-----|-------------|
198
- | Basic XSS | โœ… 100% | โœ… 95% | โœ… 90% | โœ… 82% | โŒ 20% | โŒ 8% |
199
- | Polyglot Attacks | โœ… 100% | โŒ 33% | โŒ 0% | โŒ 25% | โŒ 0% | โŒ 0% |
200
- | Encoding Bypasses | โœ… 100% | โŒ 25% | โŒ 0% | โŒ 33% | โŒ 0% | โŒ 0% |
201
- | Template Injection | โœ… 100% | โŒ 20% | โŒ 20% | โŒ 0% | โŒ 0% | โŒ 0% |
202
- | Protocol Injection | โœ… 100% | โŒ 0% | โŒ 0% | โŒ 20% | โŒ 0% | โŒ 0% |
230
+ Superseded by the two-axis benchmark above. The per-category percentages that
231
+ used to sit here came from a scoring rule that counted "output is empty" as a
232
+ win, so it rewarded deletion rather than safety and marked correct competitor
233
+ behaviour as failure. Run `pnpm test:fair` for numbers that survive scrutiny.
203
234
 
204
235
  ### Bundle Size Comparison
205
236
 
206
237
  | Library | Bundle Size | Dependencies | TypeScript |
207
238
  |---------|-------------|--------------|-------------|
208
- | **Purifai** | **~12KB** | **0** | **โœ… Native** |
239
+ | **Purifai** | **13.6KB / 4.1KB gzip** | **0** | **โœ… Native** |
209
240
  | validator.js | ~15KB | 0 | โœ… Available |
210
241
  | xss | ~25KB | 3 | โŒ None |
211
242
  | node-sanitize | ~32KB | 5 | โŒ None |
@@ -225,9 +256,10 @@ app.post('/comments', (req, res) => {
225
256
 
226
257
  ### Content Management Systems
227
258
  ```typescript
228
- // Clean rich text editor content
229
- const cleanHtml = Purifai.sanitize(editorContent, {
230
- allowBasicHtml: true,
259
+ // Reduce rich text editor content to safe plain text.
260
+ // Note: Purifai strips ALL markup. If you need to KEEP <b>/<a href>,
261
+ // use DOMPurify or sanitize-html instead - that is a different job.
262
+ const cleanText = Purifai.sanitize(editorContent, {
231
263
  maxLength: 50000
232
264
  });
233
265
  ```
@@ -272,9 +304,9 @@ const clean = sanitizeHtml(dirty, options);
272
304
 
273
305
  // After
274
306
  import { sanitize } from 'purifai';
275
- const clean = sanitize(dirty, {
276
- allowBasicHtml: true // if you need HTML tags
277
- });
307
+ const clean = sanitize(dirty);
308
+ // Purifai removes all tags. If the migration needs to KEEP safe HTML,
309
+ // stay on sanitize-html - Purifai targets the strip-to-text case.
278
310
  ```
279
311
 
280
312
  ### From xss