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 +81 -49
- package/dist/index.cjs +282 -159
- package/dist/index.d.cts +80 -25
- package/dist/index.d.ts +80 -25
- package/dist/index.js +279 -159
- package/package.json +29 -12
package/README.md
CHANGED
|
@@ -5,44 +5,54 @@
|
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](https://www.npmjs.com/package/purifai)
|
|
7
7
|
|
|
8
|
-
> **
|
|
8
|
+
> **Zero-dependency strip-to-text sanitizer with contextual output encoding**
|
|
9
9
|
|
|
10
|
-
Purifai is a lightweight, zero-dependency
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
### โ
|
|
41
|
-
-
|
|
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
|
-
-
|
|
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
|
|
71
|
+
Purifai blocks **all 84 vectors** in the corpus (64 classic + 20 modern mXSS/namespace), including:
|
|
62
72
|
|
|
63
|
-
### Critical Polyglot Attacks
|
|
73
|
+
### Critical Polyglot Attacks
|
|
64
74
|
```javascript
|
|
65
|
-
// โ
Purifai
|
|
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<b && c>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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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** |
|
|
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
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|