url-sanitizer 0.5.7 → 0.6.0
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 +93 -144
- package/dist/url-sanitizer.js +1077 -10
- package/dist/url-sanitizer.js.map +4 -4
- package/dist/url-sanitizer.min.js +8 -1
- package/dist/url-sanitizer.min.js.map +4 -4
- package/index.js +1 -6
- package/package.json +9 -6
- package/src/mjs/dompurify.esm.js +5 -0
- package/src/mjs/dompurify.js +13 -0
- package/src/mjs/uri-util.js +81 -6
package/README.md
CHANGED
|
@@ -49,47 +49,65 @@ Sanitize the given URL.
|
|
|
49
49
|
Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able.
|
|
50
50
|
|
|
51
51
|
```javascript
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// => 'http://example.com
|
|
52
|
+
// Sanitize tags and quotes
|
|
53
|
+
const res1 = await sanitizeURL('http://example.com/"onmouseover="alert(1)"?<script>alert(\'XSS\');</script>');
|
|
54
|
+
// => 'http://example.com/%26quot;onmouseover=%26quot;alert(1)%26quot;?%26lt;script%26gt;alert(%26%2339;XSS%26%2339;);%26lt;/script%26gt;'
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
console.log(decodeURIComponent(res1));
|
|
57
|
+
// => 'http://example.com/"onmouseover="alert(1)"?<script>alert('XSS');</script>'
|
|
58
|
+
|
|
59
|
+
// Parse and purify data URL
|
|
60
|
+
const res2 = await sanitizeURL('data:text/html,<div><script>alert(1);</script></div><p onclick="alert(2)"></p>', {
|
|
57
61
|
allow: ['data']
|
|
58
|
-
})
|
|
59
|
-
//
|
|
62
|
+
})
|
|
63
|
+
// -> 'data:text/html,%3Cdiv%3E%3C%2Fdiv%3E%3Cp%3E%3C%2Fp%3E'
|
|
64
|
+
|
|
65
|
+
console.log(decodeURIComponent(res2));
|
|
66
|
+
// => 'data:text/html,<div></div><p></p>'
|
|
60
67
|
|
|
61
|
-
// Can parse and
|
|
62
|
-
const base64data3 = btoa('<script>alert(1);</script>');
|
|
68
|
+
// Can also parse and purify base64 encoded data
|
|
69
|
+
const base64data3 = btoa('<div><script>alert(1);</script></div>');
|
|
63
70
|
const res3 = await sanitizeURL(`data:text/html;base64,${base64data3}`, {
|
|
64
71
|
allow: ['data']
|
|
65
|
-
})
|
|
66
|
-
// => 'data:text/html
|
|
72
|
+
})
|
|
73
|
+
// => 'data:text/html,%3Cdiv%3E%3C%2Fdiv%3E'
|
|
74
|
+
|
|
75
|
+
console.log(decodeURIComponent(res3));
|
|
76
|
+
// => 'data:text/html,<div></div>'
|
|
77
|
+
|
|
78
|
+
const base64data4 = btoa('<div><iframe src="javascript:alert(1)"></iframe></div>');
|
|
79
|
+
const res4 = await sanitizeURL(`data:text/html;base64,${base64data4}`);
|
|
80
|
+
// => 'data:text/html,%3Cdiv%3E%3C%2Fdiv%3E'
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
console.log(decodeURIComponent(res4));
|
|
83
|
+
// => 'data:text/html,<div></div>'
|
|
84
|
+
|
|
85
|
+
const res5 = await sanitizeURL('web+foo://example.com', {
|
|
69
86
|
deny: ['web+foo']
|
|
70
87
|
});
|
|
71
88
|
// => null
|
|
72
89
|
|
|
73
|
-
const
|
|
90
|
+
const res6 = await sanitizeURL('http://example.com', {
|
|
74
91
|
only: ['data', 'git', 'https']
|
|
75
92
|
});
|
|
76
93
|
// => null
|
|
77
94
|
|
|
78
|
-
const
|
|
95
|
+
const res7 = await sanitizeURL('https://example.com/"onmouseover="alert(1)"', {
|
|
79
96
|
only: ['data', 'git', 'https']
|
|
80
|
-
})
|
|
81
|
-
// => 'https://example.com
|
|
97
|
+
});
|
|
98
|
+
// => 'https://example.com/%26quot;onmouseover=%26quot;alert(1)%26quot;'
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}).then(res => decodeURIComponent(res));
|
|
86
|
-
// => 'data:text/html,<script>alert(1);</script>'
|
|
100
|
+
console.log(decodeURIComponent(res7));
|
|
101
|
+
// => 'https://example.com/"onmouseover="alert(1)"'
|
|
87
102
|
|
|
88
103
|
// `only` option also allows combinations of the specified schemes
|
|
89
|
-
const res8 = await sanitizeURL('git+https://example.com', {
|
|
104
|
+
const res8 = await sanitizeURL('git+https://example.com/foo.git?<script>alert(1)</script>', {
|
|
90
105
|
only: ['data', 'git', 'https']
|
|
91
|
-
})
|
|
92
|
-
// => 'git+https://example.com'
|
|
106
|
+
});
|
|
107
|
+
// => 'git+https://example.com/foo.git?%26lt;script%26gt;alert(1)%26lt;/script%26gt;'
|
|
108
|
+
|
|
109
|
+
console.log(decodeURIComponent(res8));
|
|
110
|
+
// => 'git+https://example.com/foo.git?<script>alert(1)</script>'
|
|
93
111
|
```
|
|
94
112
|
|
|
95
113
|
|
|
@@ -144,50 +162,52 @@ const res1 = await parseURL('javascript:alert(1)');
|
|
|
144
162
|
|
|
145
163
|
const res2 = await parseURL('https://example.com/?foo=bar#baz');
|
|
146
164
|
/* => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} */
|
|
159
|
-
|
|
160
|
-
// base64 encoded
|
|
161
|
-
const res3 = await parseURL('data:image/svg+xml;base64,
|
|
165
|
+
input: 'https://www.example.com/?foo=bar#baz',
|
|
166
|
+
valid: true,
|
|
167
|
+
data: null,
|
|
168
|
+
href: 'https://www.example.com/?foo=bar#baz',
|
|
169
|
+
origin: 'https://www.example.com',
|
|
170
|
+
protocol: 'https:',
|
|
171
|
+
hostname: 'www.example.com',
|
|
172
|
+
pathname: '/',
|
|
173
|
+
search: '?foo=bar',
|
|
174
|
+
hash: '#baz',
|
|
175
|
+
...
|
|
176
|
+
} */
|
|
177
|
+
|
|
178
|
+
// base64 encoded SVG '<svg><g onclick="alert(1)"/></svg>'
|
|
179
|
+
const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==');
|
|
162
180
|
/* => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==',
|
|
182
|
+
valid: true,
|
|
183
|
+
data: {
|
|
184
|
+
mime: 'image/svg+xml',
|
|
185
|
+
base64: false,
|
|
186
|
+
data: '%3Csvg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E'
|
|
187
|
+
},
|
|
188
|
+
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
|
|
189
|
+
origin: 'null',
|
|
190
|
+
protocol: 'data:',
|
|
191
|
+
pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
|
|
192
|
+
...
|
|
193
|
+
} */
|
|
175
194
|
|
|
176
195
|
// base64 encoded png
|
|
177
196
|
const res4 = await parseURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
|
|
178
197
|
/* => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
199
|
+
valid: true,
|
|
200
|
+
data: {
|
|
201
|
+
mime: 'image/png',
|
|
202
|
+
base64: true,
|
|
203
|
+
data: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
|
|
204
|
+
},
|
|
205
|
+
href: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
206
|
+
origin: 'null',
|
|
207
|
+
protocol: 'data:',
|
|
208
|
+
pathname: 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
209
|
+
...
|
|
210
|
+
} */
|
|
191
211
|
```
|
|
192
212
|
|
|
193
213
|
|
|
@@ -211,16 +231,19 @@ Returns **[Promise][5]<[boolean][2]>** Result.
|
|
|
211
231
|
const res1 = await isURI('https://example.com/foo');
|
|
212
232
|
// => true
|
|
213
233
|
|
|
214
|
-
const res2 = await isURI('
|
|
234
|
+
const res2 = await isURI('javascript:alert(1)');
|
|
235
|
+
// => false
|
|
236
|
+
|
|
237
|
+
const res3 = await isURI('mailto:foo@example.com');
|
|
215
238
|
// => true
|
|
216
239
|
|
|
217
|
-
const
|
|
240
|
+
const res4 = await isURI('foo:bar');
|
|
218
241
|
// => false
|
|
219
242
|
|
|
220
|
-
const
|
|
243
|
+
const res5 = await isURI('web+foo:bar');
|
|
221
244
|
// => true
|
|
222
245
|
|
|
223
|
-
const
|
|
246
|
+
const res6 = await isURI('web+javascript:alert(1)');
|
|
224
247
|
// => false
|
|
225
248
|
```
|
|
226
249
|
|
|
@@ -280,13 +303,13 @@ Add a scheme to the list of registered URI schemes.
|
|
|
280
303
|
Returns **[Array][4]<[string][1]>** Array of registered URI schemes.
|
|
281
304
|
|
|
282
305
|
```javascript
|
|
283
|
-
console.log(
|
|
306
|
+
console.log(urlSanitizer.has('foo'));
|
|
284
307
|
// => false
|
|
285
308
|
|
|
286
309
|
const res = urlSanitizer.add('foo');
|
|
287
310
|
// => ['aaa', 'aaas', 'about', 'acap', ... 'foo', ...]
|
|
288
311
|
|
|
289
|
-
console.log(
|
|
312
|
+
console.log(urlSanitizer.has('foo'));
|
|
290
313
|
// => true
|
|
291
314
|
```
|
|
292
315
|
|
|
@@ -302,13 +325,13 @@ Returns **[boolean][2]** Result.
|
|
|
302
325
|
* `true` if the scheme is successfully removed, `false` otherwise.
|
|
303
326
|
|
|
304
327
|
```javascript
|
|
305
|
-
console.log(
|
|
328
|
+
console.log(urlSanitizer.has('aaa'));
|
|
306
329
|
// => true
|
|
307
330
|
|
|
308
331
|
const res1 = urlSanitizer.remove('aaa');
|
|
309
332
|
// => true
|
|
310
333
|
|
|
311
|
-
console.log(
|
|
334
|
+
console.log(urlSanitizer.has('aaa'));
|
|
312
335
|
// => false
|
|
313
336
|
|
|
314
337
|
const res2 = urlSanitizer.remove('foo');
|
|
@@ -317,85 +340,11 @@ const res2 = urlSanitizer.remove('foo');
|
|
|
317
340
|
|
|
318
341
|
---
|
|
319
342
|
|
|
320
|
-
## Reactivate tags and purify DOM
|
|
321
|
-
|
|
322
|
-
It's outside the scope of URL Sanitizer, but escaped tags in sanitized data URL can be inconvenient.
|
|
323
|
-
For example, embedded SVG in a data URL.
|
|
324
|
-
|
|
325
|
-
For such cases, here is a sample code that reactivate tags and purify the DOM.
|
|
326
|
-
|
|
327
|
-
* [dompurify](https://www.npmjs.com/package/dompurify) is required.
|
|
328
|
-
* If you're using Node.js, [jsdom](https://www.npmjs.com/package/jsdom) is also required.
|
|
329
|
-
|
|
330
|
-
```shell
|
|
331
|
-
npm i dompurify jsdom
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
```javascript
|
|
335
|
-
import DOMPurify from 'dompurify';
|
|
336
|
-
import { JSDOM } from 'jsdom';
|
|
337
|
-
import { parseURL } from 'url-sanitizer';
|
|
338
|
-
|
|
339
|
-
const { window } = new JSDOM('');
|
|
340
|
-
const domPurify = DOMPurify(window);
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* get purified DOM from a data URL
|
|
344
|
-
*
|
|
345
|
-
* @param {string} url - URL input
|
|
346
|
-
* @returns {?string} - purified DOM, `null` if the given URL is not a data URL
|
|
347
|
-
*/
|
|
348
|
-
const getPurifiedDOMFromDataURL = async url => {
|
|
349
|
-
const parsedURL = await parseURL(url);
|
|
350
|
-
const { data: parsedDataURL } = parsedURL;
|
|
351
|
-
let purifiedDOM;
|
|
352
|
-
if (parsedDataURL) {
|
|
353
|
-
const { base64, data, mime } = parsedDataURL;
|
|
354
|
-
if (!base64 &&
|
|
355
|
-
/^(?:text\/(?:ht|x)ml|application\/(?:xhtml\+)?xml|image\/svg\+xml)/
|
|
356
|
-
.test(mime)) {
|
|
357
|
-
let parsedData = data;
|
|
358
|
-
const matchedHTMLChars =
|
|
359
|
-
parsedData.matchAll(/%26(?:(?:l|g|quo)t|%2339);?/g);
|
|
360
|
-
const items = [...matchedHTMLChars].reverse();
|
|
361
|
-
const unescapeURLEncodedHTMLChars = ch => {
|
|
362
|
-
let unescapedChar;
|
|
363
|
-
if (/%26lt;?/.test(ch)) {
|
|
364
|
-
unescapedChar = '<';
|
|
365
|
-
} else if (/%26gt;?/.test(ch)) {
|
|
366
|
-
unescapedChar = '>';
|
|
367
|
-
} else if (/%26quot;?/.test(ch)) {
|
|
368
|
-
unescapedChar = '"';
|
|
369
|
-
} else if (/%26%2339;?/.test(ch)) {
|
|
370
|
-
unescapedChar = "'";
|
|
371
|
-
} else {
|
|
372
|
-
unescapedChar = ch;
|
|
373
|
-
}
|
|
374
|
-
return unescapedChar;
|
|
375
|
-
};
|
|
376
|
-
for (const item of items) {
|
|
377
|
-
const [htmlChar] = item;
|
|
378
|
-
const { index } = item;
|
|
379
|
-
const [preHTMLChar, postHTMLChar] = [
|
|
380
|
-
parsedData.substring(0, index),
|
|
381
|
-
parsedData.substring(index + htmlChar.length)
|
|
382
|
-
];
|
|
383
|
-
const unescapedHTMLChar = unescapeURLEncodedHTMLChars(htmlChar);
|
|
384
|
-
parsedData = `${preHTMLChar}${unescapedHTMLChar}${postHTMLChar}`;
|
|
385
|
-
}
|
|
386
|
-
purifiedDOM = domPurify.sanitize(decodeURIComponent(parsedData));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return purifiedDOM ?? null;
|
|
390
|
-
};
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
343
|
## Acknowledgments
|
|
396
344
|
|
|
397
345
|
The following resources have been of great help in the development of the URL Sanitizer.
|
|
398
346
|
|
|
347
|
+
* [DOMPurify](https://www.npmjs.com/package/dompurify?activeTab=readme)
|
|
399
348
|
* [Uniform Resource Identifier (URI) Schemes - IANA](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml)
|
|
400
349
|
* [Encoding -- determine the character encoding of a text file. - file/file](https://github.com/file/file/blob/master/src/encoding.c)
|
|
401
350
|
|