url-sanitizer 0.5.7 → 0.6.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
@@ -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
- const res1 = await sanitizeURL('http://example.com/?<script>alert(1);</script>')
53
- .then(res => decodeURIComponent(res));
54
- // => 'http://example.com/?&lt;script&gt;alert(1);&lt;/script&gt;'
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
- const res2 = await sanitizeURL('data:text/html,<script>alert(1);</script>', {
56
+ console.log(decodeURIComponent(res1));
57
+ // => 'http://example.com/&quot;onmouseover=&quot;alert(1)&quot;?&lt;script&gt;alert(&#39;XSS&#39;);&lt;/script&gt;'
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
- }).then(res => decodeURIComponent(res));
59
- // => 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
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 sanitize base64 encoded data
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
- }).then(res => decodeURIComponent(res));
66
- // => 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
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
- const res4 = await sanitizeURL('web+foo://example.com', {
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 res5 = await sanitizeURL('http://example.com', {
90
+ const res6 = await sanitizeURL('http://example.com', {
74
91
  only: ['data', 'git', 'https']
75
92
  });
76
93
  // => null
77
94
 
78
- const res6 = await sanitizeURL('https://example.com/"onmouseover="alert(1)"', {
95
+ const res7 = await sanitizeURL('https://example.com/"onmouseover="alert(1)"', {
79
96
  only: ['data', 'git', 'https']
80
- }).then(res => decodeURIComponent(res));
81
- // => 'https://example.com/&quot;onmouseover=&quot;alert(1)&quot;'
97
+ });
98
+ // => 'https://example.com/%26quot;onmouseover=%26quot;alert(1)%26quot;'
82
99
 
83
- const res7 = await sanitizeURL('data:text/html,<script>alert(1);</script>', {
84
- only: ['data', 'git', 'https']
85
- }).then(res => decodeURIComponent(res));
86
- // => 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
100
+ console.log(decodeURIComponent(res7));
101
+ // => 'https://example.com/&quot;onmouseover=&quot;alert(1)&quot;'
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
- }).then(res => decodeURIComponent(res));;
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?&lt;script&gt;alert(1)&lt;/script&gt;'
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
- input: 'https://www.example.com/?foo=bar#baz',
148
- valid: true,
149
- data: null,
150
- href: 'https://www.example.com/?foo=bar#baz',
151
- origin: 'https://www.example.com',
152
- protocol: 'https:',
153
- hostname: 'www.example.com',
154
- pathname: '/',
155
- search: '?foo=bar',
156
- hash: '#baz',
157
- ...
158
- } */
159
-
160
- // base64 encoded svg '<svg><g onload="alert(1)"/></svg>'
161
- const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmxvYWQ9ImFsZXJ0KDEpIi8+PC9zdmc+');
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
- input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmxvYWQ9ImFsZXJ0KDEpIi8+PC9zdmc+',
164
- valid: true,
165
- data: {
166
- mime: 'image/svg+xml',
167
- base64: false,
168
- data: '%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;'
169
- },
170
- href: 'data:image/svg+xml,%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;',
171
- protocol: 'data:',
172
- pathname: 'image/svg+xml,%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;',
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
- input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
180
- valid: true,
181
- data: {
182
- mime: 'image/png',
183
- base64: true,
184
- data: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
185
- },
186
- href: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
187
- protocol: 'data:',
188
- pathname: 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
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('mailto:foo@example.com');
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 res3 = await isURI('foo:bar');
240
+ const res4 = await isURI('foo:bar');
218
241
  // => false
219
242
 
220
- const res4 = await isURI('web+foo:bar');
243
+ const res5 = await isURI('web+foo:bar');
221
244
  // => true
222
245
 
223
- const res5 = await isURI('web+javascript:alert(1)');
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(isURISync('foo'));
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(isURISync('foo'));
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(isURISync('aaa'));
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(isURISync('aaa'));
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