url-sanitizer 0.4.3 → 0.5.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 +166 -34
- package/dist/url-sanitizer.js +121 -32
- package/dist/url-sanitizer.js.map +2 -2
- package/dist/url-sanitizer.min.js +1 -1
- package/dist/url-sanitizer.min.js.map +3 -3
- package/package.json +3 -3
- package/src/mjs/uri-util.js +149 -39
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
URL sanitizer for Node.js (>=18), browsers and web sites.
|
|
9
9
|
**Experimental**
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
## Install
|
|
12
13
|
|
|
13
14
|
```shell
|
|
@@ -20,14 +21,16 @@ For browsers and web sites, standalone ESM builds are available in `dist/` direc
|
|
|
20
21
|
|
|
21
22
|
Or, download them from [Releases](https://github.com/asamuzaK/urlSanitizer/releases).
|
|
22
23
|
|
|
24
|
+
|
|
23
25
|
## Usage
|
|
24
26
|
|
|
25
27
|
```javascript
|
|
26
28
|
import urlSanitizer, {
|
|
27
|
-
isURI, isURISync, sanitizeURL, sanitizeURLSync
|
|
29
|
+
isURI, isURISync, parseURL, parseURLSync, sanitizeURL, sanitizeURLSync
|
|
28
30
|
} from 'url-sanitizer';
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
|
|
31
34
|
## sanitizeURL(url, opt)
|
|
32
35
|
|
|
33
36
|
Sanitize the given URL.
|
|
@@ -35,69 +38,198 @@ Sanitize the given URL.
|
|
|
35
38
|
|
|
36
39
|
### Parameters
|
|
37
40
|
|
|
38
|
-
* `url` **[string][1]** URL input
|
|
39
|
-
* `opt` **[object][3]** Options
|
|
40
|
-
* `opt.allow` **[Array][4]<[string][1]>** Array of allowed schemes, e.g. `['data']
|
|
41
|
-
* `opt.deny` **[Array][4]<[string][1]>** Array of denied schemes, e.g. `['web+foo']
|
|
41
|
+
* `url` **[string][1]** URL input.
|
|
42
|
+
* `opt` **[object][3]** Options.
|
|
43
|
+
* `opt.allow` **[Array][4]<[string][1]>** Array of allowed schemes, e.g. `['data']`.
|
|
44
|
+
* `opt.deny` **[Array][4]<[string][1]>** Array of denied schemes, e.g. `['web+foo']`.
|
|
45
|
+
* `opt.only` **[Array][4]<[string][1]>** Array of specific schemes to allow, e.g. `['git', 'https']`.
|
|
46
|
+
`only` takes precedence over `allow` and `deny`.
|
|
42
47
|
|
|
43
|
-
Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able
|
|
48
|
+
Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able.
|
|
44
49
|
|
|
45
50
|
```javascript
|
|
46
51
|
const res1 = await sanitizeURL('http://example.com/?<script>alert(1);</script>')
|
|
47
52
|
.then(res => decodeURIComponent(res));
|
|
48
|
-
//
|
|
53
|
+
// => 'http://example.com/?<script>alert(1);</script>'
|
|
49
54
|
|
|
50
55
|
const res2 = await sanitizeURL('data:text/html,<script>alert(1);</script>', {
|
|
51
56
|
allow: ['data']
|
|
52
57
|
}).then(res => decodeURIComponent(res));
|
|
53
|
-
//
|
|
58
|
+
// => 'data:text/html,<script>alert(1);</script>'
|
|
54
59
|
|
|
55
60
|
// Can parse and sanitize base64 encoded data
|
|
56
61
|
const base64data3 = btoa('<script>alert(1);</script>');
|
|
57
62
|
const res3 = await sanitizeURL(`data:text/html;base64,${base64data3}`, {
|
|
58
63
|
allow: ['data']
|
|
59
64
|
}).then(res => decodeURIComponent(res));
|
|
60
|
-
//
|
|
65
|
+
// => 'data:text/html,<script>alert(1);</script>'
|
|
61
66
|
|
|
62
67
|
const res4 = await sanitizeURL('web+foo://example.com', {
|
|
63
68
|
deny: ['web+foo']
|
|
64
69
|
});
|
|
65
|
-
//
|
|
70
|
+
// => null
|
|
71
|
+
|
|
72
|
+
const res5 = await sanitizeURL('http://example.com', {
|
|
73
|
+
only: ['data', 'git', 'https']
|
|
74
|
+
});
|
|
75
|
+
// => null
|
|
76
|
+
|
|
77
|
+
const res6 = await sanitizeURL('https://example.com/"onmouseover="alert(1)"', {
|
|
78
|
+
only: ['data', 'git', 'https']
|
|
79
|
+
}).then(res => decodeURIComponent(res));
|
|
80
|
+
// => https://example.com/"onmouseover="alert(1)"
|
|
81
|
+
|
|
82
|
+
const res7 = await sanitizeURL('data:text/html,<script>alert(1);</script>', {
|
|
83
|
+
only: ['data', 'git', 'https']
|
|
84
|
+
}).then(res => decodeURIComponent(res));
|
|
85
|
+
// => 'data:text/html,<script>alert(1);</script>'
|
|
86
|
+
|
|
87
|
+
// `only` option also allows combinations of the specified schemes
|
|
88
|
+
const res8 = await sanitizeURL('git+https://example.com', {
|
|
89
|
+
only: ['data', 'git', 'https']
|
|
90
|
+
}).then(res => decodeURIComponent(res));;
|
|
91
|
+
// => git+https://example.com
|
|
66
92
|
```
|
|
67
93
|
|
|
94
|
+
|
|
68
95
|
## sanitizeURLSync
|
|
69
96
|
|
|
70
97
|
Synchronous version of the [sanitizeURL()](#sanitizeurlurl-opt).
|
|
71
98
|
|
|
99
|
+
|
|
100
|
+
## parseURL(url)
|
|
101
|
+
|
|
102
|
+
Parse the given URL.
|
|
103
|
+
|
|
104
|
+
### Parameters
|
|
105
|
+
|
|
106
|
+
* `url` **[string][1]** URL input.
|
|
107
|
+
|
|
108
|
+
Returns **[Promise][5]<[ParsedURL](#parsedurl)>** Result.
|
|
109
|
+
|
|
110
|
+
### ParsedURL
|
|
111
|
+
|
|
112
|
+
Object with extended properties based on [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API.
|
|
113
|
+
|
|
114
|
+
Type: [object][3]
|
|
115
|
+
|
|
116
|
+
#### Properties
|
|
117
|
+
|
|
118
|
+
* `input` **[string][1]** URL input.
|
|
119
|
+
* `valid` **[boolean][2]** Is valid URI.
|
|
120
|
+
* `data` **[object][3]** Parsed result of data URL, `null`able.
|
|
121
|
+
* `data.mime` **[string][1]** MIME type.
|
|
122
|
+
* `data.base64` **[boolean][2]** `true` if base64 encoded.
|
|
123
|
+
* `data.data` **[string][1]** Data part of the data URL.
|
|
124
|
+
* `href` **[string][1]** Same as URL API.
|
|
125
|
+
* `origin` **[string][1]** Same as URL API.
|
|
126
|
+
* `protocol` **[string][1]** Same as URL API.
|
|
127
|
+
* `username` **[string][1]** Same as URL API.
|
|
128
|
+
* `password` **[string][1]** Same as URL API.
|
|
129
|
+
* `host` **[string][1]** Same as URL API.
|
|
130
|
+
* `hostname` **[string][1]** Same as URL API.
|
|
131
|
+
* `port` **[string][1]** Same as URL API.
|
|
132
|
+
* `pathname` **[string][1]** Same as URL API.
|
|
133
|
+
* `search` **[string][1]** Same as URL API.
|
|
134
|
+
* `searchParams` **[object][3]** Same as URL API.
|
|
135
|
+
* `hash` **[string][1]** Same as URL API.
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const res1 = await parseURL('javascript:alert(1)');
|
|
139
|
+
/* => {
|
|
140
|
+
input: 'javascript:alert(1)',
|
|
141
|
+
valid: false
|
|
142
|
+
} */
|
|
143
|
+
|
|
144
|
+
const res2 = await parseURL('https://example.com/?foo=bar#baz');
|
|
145
|
+
/* => {
|
|
146
|
+
input: 'https://www.example.com/?foo=bar#baz',
|
|
147
|
+
valid: true,
|
|
148
|
+
data: null,
|
|
149
|
+
href: 'https://www.example.com/?foo=bar#baz',
|
|
150
|
+
origin: 'https://www.example.com',
|
|
151
|
+
protocol: 'https:',
|
|
152
|
+
hostname: 'www.example.com',
|
|
153
|
+
pathname: '/',
|
|
154
|
+
search: '?foo=bar',
|
|
155
|
+
hash: '#baz',
|
|
156
|
+
...
|
|
157
|
+
} */
|
|
158
|
+
|
|
159
|
+
// base64 encoded svg '<svg><g onload="alert(1)"/></svg>'
|
|
160
|
+
const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmxvYWQ9ImFsZXJ0KDEpIi8+PC9zdmc+');
|
|
161
|
+
/* => {
|
|
162
|
+
input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmxvYWQ9ImFsZXJ0KDEpIi8+PC9zdmc+',
|
|
163
|
+
valid: true,
|
|
164
|
+
data: {
|
|
165
|
+
mime: 'image/svg+xml',
|
|
166
|
+
base64: false,
|
|
167
|
+
data: '%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;'
|
|
168
|
+
},
|
|
169
|
+
href: 'data:image/svg+xml,%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;',
|
|
170
|
+
protocol: 'data:',
|
|
171
|
+
pathname: 'image/svg+xml,%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;',
|
|
172
|
+
...
|
|
173
|
+
} */
|
|
174
|
+
|
|
175
|
+
// base64 encoded png
|
|
176
|
+
const res4 = await parseURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
|
|
177
|
+
/* => {
|
|
178
|
+
input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
179
|
+
valid: true,
|
|
180
|
+
data: {
|
|
181
|
+
mime: 'image/png',
|
|
182
|
+
base64: true,
|
|
183
|
+
data: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
|
|
184
|
+
},
|
|
185
|
+
href: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
186
|
+
protocol: 'data:',
|
|
187
|
+
pathname: 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
188
|
+
...
|
|
189
|
+
} */
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## parseURLSync(url)
|
|
194
|
+
|
|
195
|
+
Synchronous version of the [parseURL()](#parseURL).
|
|
196
|
+
|
|
197
|
+
|
|
72
198
|
## isURI(uri)
|
|
73
199
|
|
|
74
200
|
Determines whether the given URI is valid.
|
|
75
201
|
|
|
76
202
|
### Parameters
|
|
77
203
|
|
|
78
|
-
* `uri` **[string][1]** URI input
|
|
204
|
+
* `uri` **[string][1]** URI input.
|
|
79
205
|
|
|
80
|
-
Returns **[Promise][5]<[boolean][2]>** Result
|
|
81
|
-
* Always `true` for `web+*` and `ext+*` schemes
|
|
206
|
+
Returns **[Promise][5]<[boolean][2]>** Result.
|
|
207
|
+
* Always `true` for `web+*` and `ext+*` schemes, except `web+javascript`, `web+vbscript`, `ext+javascript`, `ext+vbscript`.
|
|
82
208
|
|
|
83
209
|
```javascript
|
|
84
210
|
const res1 = await isURI('https://example.com/foo');
|
|
85
|
-
//
|
|
211
|
+
// => true
|
|
86
212
|
|
|
87
213
|
const res2 = await isURI('mailto:foo@example.com');
|
|
88
|
-
//
|
|
214
|
+
// => true
|
|
89
215
|
|
|
90
216
|
const res3 = await isURI('foo:bar');
|
|
91
|
-
//
|
|
217
|
+
// => false
|
|
92
218
|
|
|
93
219
|
const res4 = await isURI('web+foo:bar');
|
|
94
|
-
//
|
|
220
|
+
// => true
|
|
221
|
+
|
|
222
|
+
const res5 = await isURI('web+javascript:alert(1)');
|
|
223
|
+
// => false
|
|
95
224
|
```
|
|
96
225
|
|
|
226
|
+
|
|
97
227
|
## isURISync(uri)
|
|
98
228
|
|
|
99
229
|
Synchronous version of the [isURI()](#isuriuri).
|
|
100
230
|
|
|
231
|
+
---
|
|
232
|
+
|
|
101
233
|
## urlSanitizer
|
|
102
234
|
|
|
103
235
|
Instance of the sanitizer.
|
|
@@ -108,11 +240,11 @@ Get an array of URI schemes registered at [iana.org](https://www.iana.org/assign
|
|
|
108
240
|
* Historical schemes omitted.
|
|
109
241
|
* `moz-extension` scheme added.
|
|
110
242
|
|
|
111
|
-
Returns **[Array][4]<[string][1]>** Array of registered URI schemes
|
|
243
|
+
Returns **[Array][4]<[string][1]>** Array of registered URI schemes.
|
|
112
244
|
|
|
113
245
|
```javascript
|
|
114
246
|
const schemes = urlSanitizer.get();
|
|
115
|
-
//
|
|
247
|
+
// => ['aaa', 'aaas', 'about', 'acap', 'acct', ...];
|
|
116
248
|
```
|
|
117
249
|
|
|
118
250
|
### urlSanitizer.has(scheme)
|
|
@@ -121,16 +253,16 @@ Check if the given scheme is registered.
|
|
|
121
253
|
|
|
122
254
|
#### Parameters
|
|
123
255
|
|
|
124
|
-
* `scheme` **[string][1]** Scheme
|
|
256
|
+
* `scheme` **[string][1]** Scheme.
|
|
125
257
|
|
|
126
|
-
Returns **[boolean][2]** Result
|
|
258
|
+
Returns **[boolean][2]** Result.
|
|
127
259
|
|
|
128
260
|
```javascript
|
|
129
261
|
const res1 = urlSanitizer.has('https');
|
|
130
|
-
//
|
|
262
|
+
// => true
|
|
131
263
|
|
|
132
264
|
const res2 = urlSanitizer.has('foo');
|
|
133
|
-
//
|
|
265
|
+
// => false
|
|
134
266
|
```
|
|
135
267
|
|
|
136
268
|
### urlSanitizer.add(scheme)
|
|
@@ -140,19 +272,19 @@ Add a scheme to the list of URI schemes.
|
|
|
140
272
|
|
|
141
273
|
#### Parameters
|
|
142
274
|
|
|
143
|
-
* `scheme` **[string][1]** Scheme
|
|
275
|
+
* `scheme` **[string][1]** Scheme.
|
|
144
276
|
|
|
145
|
-
Returns **[Array][4]<[string][1]>** Array of registered URI schemes
|
|
277
|
+
Returns **[Array][4]<[string][1]>** Array of registered URI schemes.
|
|
146
278
|
|
|
147
279
|
```javascript
|
|
148
280
|
console.log(isURISync('foo'));
|
|
149
|
-
//
|
|
281
|
+
// => false;
|
|
150
282
|
|
|
151
283
|
const res = urlSanitizer.add('foo');
|
|
152
|
-
//
|
|
284
|
+
// => ['aaa', 'aaas', 'about', 'acap', ... 'foo', ...];
|
|
153
285
|
|
|
154
286
|
console.log(isURISync('foo'));
|
|
155
|
-
//
|
|
287
|
+
// => true;
|
|
156
288
|
```
|
|
157
289
|
|
|
158
290
|
### urlSanitizer.remove(scheme)
|
|
@@ -161,23 +293,23 @@ Remove a scheme from the list of URI schemes.
|
|
|
161
293
|
|
|
162
294
|
#### Parameters
|
|
163
295
|
|
|
164
|
-
* `scheme` **[string][1]** Scheme
|
|
296
|
+
* `scheme` **[string][1]** Scheme.
|
|
165
297
|
|
|
166
|
-
Returns **[boolean][2]** Result
|
|
298
|
+
Returns **[boolean][2]** Result.
|
|
167
299
|
* `true` if the scheme is successfully removed, `false` otherwise.
|
|
168
300
|
|
|
169
301
|
```javascript
|
|
170
302
|
console.log(isURISync('aaa'));
|
|
171
|
-
//
|
|
303
|
+
// => true;
|
|
172
304
|
|
|
173
305
|
const res1 = urlSanitizer.remove('aaa');
|
|
174
|
-
//
|
|
306
|
+
// => true
|
|
175
307
|
|
|
176
308
|
console.log(isURISync('aaa'));
|
|
177
|
-
//
|
|
309
|
+
// => false;
|
|
178
310
|
|
|
179
311
|
const res2 = urlSanitizer.remove('foo');
|
|
180
|
-
//
|
|
312
|
+
// => false
|
|
181
313
|
```
|
|
182
314
|
|
|
183
315
|
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
|
package/dist/url-sanitizer.js
CHANGED
|
@@ -591,14 +591,17 @@ var uri_schemes_default = [
|
|
|
591
591
|
|
|
592
592
|
// src/mjs/uri-util.js
|
|
593
593
|
var HEX = 16;
|
|
594
|
-
var
|
|
594
|
+
var REG_BASE64 = /^[\da-z+/\-_=]+$/i;
|
|
595
595
|
var REG_DATA_URL = /data:[^,]*,[^"]+/g;
|
|
596
|
-
var REG_DATA_URL_BASE64 = /data:[^,]*;?base64,[\
|
|
597
|
-
var
|
|
596
|
+
var REG_DATA_URL_BASE64 = /data:[^,]*;?base64,[\da-z+/\-_=]+/i;
|
|
597
|
+
var REG_HTML_SP = /[<>"'\s]/g;
|
|
598
|
+
var REG_HTML_SP_URL_ENC = /%(?:2(?:2|7)|3(?:C|E))/g;
|
|
598
599
|
var REG_NUM_REF = /&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/ig;
|
|
599
|
-
var REG_SCHEME = /^[a-z][
|
|
600
|
+
var REG_SCHEME = /^[a-z][\da-z+\-.]*$/;
|
|
600
601
|
var REG_SCHEME_CUSTOM = /^(?:ext|web)\+[a-z]+$/;
|
|
601
602
|
var REG_SCRIPT = /(?:java|vb)script/;
|
|
603
|
+
var REG_URL_ENC = /^%[\dA-F]{2}$/i;
|
|
604
|
+
var REG_URL_ENC_AMP = /%26/g;
|
|
602
605
|
var getUrlEncodedString = (str) => {
|
|
603
606
|
if (!isString(str)) {
|
|
604
607
|
throw new TypeError(`Expected String but got ${getType(str)}.`);
|
|
@@ -611,7 +614,7 @@ var getUrlEncodedString = (str) => {
|
|
|
611
614
|
};
|
|
612
615
|
var escapeUrlEncodedHtmlChars = (ch) => {
|
|
613
616
|
if (isString(ch)) {
|
|
614
|
-
if (
|
|
617
|
+
if (REG_URL_ENC.test(ch)) {
|
|
615
618
|
ch = ch.toUpperCase();
|
|
616
619
|
} else {
|
|
617
620
|
throw new Error(`Invalid URL encoded character: ${ch}`);
|
|
@@ -639,14 +642,14 @@ var escapeUrlEncodedHtmlChars = (ch) => {
|
|
|
639
642
|
var parseBase64 = (data) => {
|
|
640
643
|
if (!isString(data)) {
|
|
641
644
|
throw new TypeError(`Expected String but got ${getType(data)}.`);
|
|
642
|
-
} else if (
|
|
645
|
+
} else if (!REG_BASE64.test(data)) {
|
|
643
646
|
throw new Error(`Invalid base64 data: ${data}`);
|
|
644
647
|
}
|
|
645
648
|
const bin = atob(data);
|
|
646
649
|
const uint8arr = Uint8Array.from([...bin].map((c) => c.charCodeAt(0)));
|
|
647
|
-
const
|
|
650
|
+
const textCharCodes = new Set(text_chars_default);
|
|
648
651
|
let parsedData;
|
|
649
|
-
if (uint8arr.every((c) =>
|
|
652
|
+
if (uint8arr.every((c) => textCharCodes.has(c))) {
|
|
650
653
|
parsedData = bin.replace(/\s/g, getUrlEncodedString);
|
|
651
654
|
} else {
|
|
652
655
|
parsedData = data;
|
|
@@ -664,7 +667,7 @@ var parseUrlEncodedNumCharRef = (str, nest = 0) => {
|
|
|
664
667
|
}
|
|
665
668
|
let res = decodeURIComponent(str);
|
|
666
669
|
if (/&#/.test(res)) {
|
|
667
|
-
const
|
|
670
|
+
const textCharCodes = new Set(text_chars_default);
|
|
668
671
|
const items = [...res.matchAll(REG_NUM_REF)].reverse();
|
|
669
672
|
for (const item of items) {
|
|
670
673
|
const [numCharRef, value] = item;
|
|
@@ -680,9 +683,9 @@ var parseUrlEncodedNumCharRef = (str, nest = 0) => {
|
|
|
680
683
|
res.substring(0, index),
|
|
681
684
|
res.substring(index + numCharRef.length)
|
|
682
685
|
];
|
|
683
|
-
if (
|
|
686
|
+
if (textCharCodes.has(num)) {
|
|
684
687
|
res = `${preNum}${String.fromCharCode(num)}${postNum}`;
|
|
685
|
-
if (/#x?$/.test(preNum) || /^#x?[
|
|
688
|
+
if (/#x?$/.test(preNum) || /^#(?:x(?:00)?[2-7]|\d)/.test(postNum)) {
|
|
686
689
|
res = parseUrlEncodedNumCharRef(res, ++nest);
|
|
687
690
|
}
|
|
688
691
|
} else if (num < HEX * HEX) {
|
|
@@ -789,16 +792,17 @@ var URLSanitizer = class extends URISchemes {
|
|
|
789
792
|
* @param {object} opt - options
|
|
790
793
|
* @param {Array.<string>} opt.allow - array of allowed schemes
|
|
791
794
|
* @param {Array.<string>} opt.deny - array of denied schemes
|
|
795
|
+
* @param {Array.<string>} opt.only - array of specific schemes to allow
|
|
792
796
|
* @returns {?string} - sanitized URL
|
|
793
797
|
*/
|
|
794
|
-
sanitize(url, opt = { allow: [], deny: [] }) {
|
|
798
|
+
sanitize(url, opt = { allow: [], deny: [], only: [] }) {
|
|
795
799
|
if (this.#nest > HEX) {
|
|
796
800
|
this.#nest = 0;
|
|
797
801
|
throw new Error("Data URLs nested too deeply.");
|
|
798
802
|
}
|
|
799
803
|
let sanitizedUrl;
|
|
800
804
|
if (super.isURI(url)) {
|
|
801
|
-
const { allow, deny } = opt ?? {};
|
|
805
|
+
const { allow, deny, only } = opt ?? {};
|
|
802
806
|
const { hash, href, pathname, protocol, search } = new URL(url);
|
|
803
807
|
const scheme = protocol.replace(/:$/, "");
|
|
804
808
|
const schemeParts = scheme.split("+");
|
|
@@ -808,8 +812,12 @@ var URLSanitizer = class extends URISchemes {
|
|
|
808
812
|
["javascrpt", false],
|
|
809
813
|
["vbscript", false]
|
|
810
814
|
]);
|
|
811
|
-
if (Array.isArray(
|
|
812
|
-
const
|
|
815
|
+
if (Array.isArray(only) && only.length) {
|
|
816
|
+
const schemes = super.get();
|
|
817
|
+
for (const item of schemes) {
|
|
818
|
+
schemeMap.set(item, false);
|
|
819
|
+
}
|
|
820
|
+
const items = Object.values(only);
|
|
813
821
|
for (let item of items) {
|
|
814
822
|
if (isString(item)) {
|
|
815
823
|
item = item.trim();
|
|
@@ -818,14 +826,26 @@ var URLSanitizer = class extends URISchemes {
|
|
|
818
826
|
}
|
|
819
827
|
}
|
|
820
828
|
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
+
} else {
|
|
830
|
+
if (Array.isArray(allow) && allow.length) {
|
|
831
|
+
const items = Object.values(allow);
|
|
832
|
+
for (let item of items) {
|
|
833
|
+
if (isString(item)) {
|
|
834
|
+
item = item.trim();
|
|
835
|
+
if (!REG_SCRIPT.test(item)) {
|
|
836
|
+
schemeMap.set(item, true);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (Array.isArray(deny) && deny.length) {
|
|
842
|
+
const items = Object.values(deny);
|
|
843
|
+
for (let item of items) {
|
|
844
|
+
if (isString(item)) {
|
|
845
|
+
item = item.trim();
|
|
846
|
+
if (item) {
|
|
847
|
+
schemeMap.set(item, false);
|
|
848
|
+
}
|
|
829
849
|
}
|
|
830
850
|
}
|
|
831
851
|
}
|
|
@@ -838,15 +858,12 @@ var URLSanitizer = class extends URISchemes {
|
|
|
838
858
|
}
|
|
839
859
|
}
|
|
840
860
|
if (bool) {
|
|
841
|
-
const [amp, lt, gt, quot, apos] = ["&", "<", ">", '"', "'"].map(getUrlEncodedString);
|
|
842
|
-
const regAmp = new RegExp(amp, "g");
|
|
843
|
-
const regEncodedChars = new RegExp(`(${lt}|${gt}|${quot}|${apos})`, "g");
|
|
844
861
|
let escapeHtml;
|
|
845
862
|
let urlToSanitize = href;
|
|
846
863
|
if (schemeParts.includes("data")) {
|
|
847
|
-
const [
|
|
864
|
+
const [head, ...body] = pathname.split(",");
|
|
848
865
|
const data = `${body.join(",")}${search}${hash}`;
|
|
849
|
-
const mediaType =
|
|
866
|
+
const mediaType = head.split(";");
|
|
850
867
|
let parsedData = data;
|
|
851
868
|
if (mediaType[mediaType.length - 1] === "base64") {
|
|
852
869
|
mediaType.pop();
|
|
@@ -862,7 +879,7 @@ var URLSanitizer = class extends URISchemes {
|
|
|
862
879
|
} catch (e) {
|
|
863
880
|
}
|
|
864
881
|
}
|
|
865
|
-
const containsDataUrl =
|
|
882
|
+
const containsDataUrl = /data:[^,]*,/.test(parsedData);
|
|
866
883
|
if (parsedData !== data || containsDataUrl) {
|
|
867
884
|
if (containsDataUrl) {
|
|
868
885
|
const matchedDataUrls = parsedData.matchAll(REG_DATA_URL);
|
|
@@ -906,9 +923,9 @@ var URLSanitizer = class extends URISchemes {
|
|
|
906
923
|
escapeHtml = true;
|
|
907
924
|
}
|
|
908
925
|
if (urlToSanitize) {
|
|
909
|
-
sanitizedUrl = urlToSanitize.replace(
|
|
926
|
+
sanitizedUrl = urlToSanitize.replace(REG_HTML_SP, getUrlEncodedString).replace(REG_URL_ENC_AMP, escapeUrlEncodedHtmlChars);
|
|
910
927
|
if (escapeHtml) {
|
|
911
|
-
sanitizedUrl = sanitizedUrl.replace(
|
|
928
|
+
sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC, escapeUrlEncodedHtmlChars);
|
|
912
929
|
this.#nest = 0;
|
|
913
930
|
}
|
|
914
931
|
} else {
|
|
@@ -919,6 +936,77 @@ var URLSanitizer = class extends URISchemes {
|
|
|
919
936
|
}
|
|
920
937
|
return sanitizedUrl || null;
|
|
921
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* object with extended props based on URL API
|
|
941
|
+
*
|
|
942
|
+
* @typedef {object} ParsedURL
|
|
943
|
+
* @property {string} input - URL input
|
|
944
|
+
* @property {boolean} valid - is valid URI
|
|
945
|
+
* @property {object} data - parsed result of data URL, `null`able
|
|
946
|
+
* @property {string} data.mime - MIME type
|
|
947
|
+
* @property {boolean} data.base64 - `true` if base64 encoded
|
|
948
|
+
* @property {string} data.data - data part of the data URL
|
|
949
|
+
* @property {string} href - same as URL API
|
|
950
|
+
* @property {string} origin - same as URL API
|
|
951
|
+
* @property {string} protocol - same as URL API
|
|
952
|
+
* @property {string} username - same as URL API
|
|
953
|
+
* @property {string} password - same as URL API
|
|
954
|
+
* @property {string} host - same as URL API
|
|
955
|
+
* @property {string} hostname - same as URL API
|
|
956
|
+
* @property {string} port - same as URL API
|
|
957
|
+
* @property {string} pathname - same as URL API
|
|
958
|
+
* @property {string} search - same as URL API
|
|
959
|
+
* @property {object} searchParams - same as URL API
|
|
960
|
+
* @property {string} hash - same as URL API
|
|
961
|
+
*/
|
|
962
|
+
/**
|
|
963
|
+
* parse sanitized URL
|
|
964
|
+
*
|
|
965
|
+
* @param {string} url - URL input
|
|
966
|
+
* @returns {ParsedURL} - result with extended props based on URL API
|
|
967
|
+
*/
|
|
968
|
+
parse(url) {
|
|
969
|
+
if (!isString(url)) {
|
|
970
|
+
throw new TypeError(`Expected String but got ${getType(url)}.`);
|
|
971
|
+
}
|
|
972
|
+
const sanitizedUrl = this.sanitize(url, {
|
|
973
|
+
allow: ["data", "file"]
|
|
974
|
+
});
|
|
975
|
+
const parsedUrl = /* @__PURE__ */ new Map([
|
|
976
|
+
["input", url]
|
|
977
|
+
]);
|
|
978
|
+
if (sanitizedUrl) {
|
|
979
|
+
const urlObj = new URL(sanitizedUrl);
|
|
980
|
+
const { pathname, protocol } = urlObj;
|
|
981
|
+
const schemeParts = protocol.replace(/:$/, "").split("+");
|
|
982
|
+
parsedUrl.set("valid", true);
|
|
983
|
+
if (schemeParts.includes("data")) {
|
|
984
|
+
const dataUrl = /* @__PURE__ */ new Map();
|
|
985
|
+
const [head, ...body] = pathname.split(",");
|
|
986
|
+
const data = `${body.join(",")}`;
|
|
987
|
+
const mediaType = head.split(";");
|
|
988
|
+
const isBase64 = mediaType[mediaType.length - 1] === "base64";
|
|
989
|
+
if (isBase64) {
|
|
990
|
+
mediaType.pop();
|
|
991
|
+
}
|
|
992
|
+
dataUrl.set("mime", mediaType.join(";"));
|
|
993
|
+
dataUrl.set("base64", isBase64);
|
|
994
|
+
dataUrl.set("data", data);
|
|
995
|
+
parsedUrl.set("data", Object.fromEntries(dataUrl));
|
|
996
|
+
} else {
|
|
997
|
+
parsedUrl.set("data", null);
|
|
998
|
+
}
|
|
999
|
+
for (const key in urlObj) {
|
|
1000
|
+
const value = urlObj[key];
|
|
1001
|
+
if (typeof value !== "function") {
|
|
1002
|
+
parsedUrl.set(key, value);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
} else {
|
|
1006
|
+
parsedUrl.set("valid", false);
|
|
1007
|
+
}
|
|
1008
|
+
return Object.fromEntries(parsedUrl);
|
|
1009
|
+
}
|
|
922
1010
|
};
|
|
923
1011
|
var urlSanitizer = new URLSanitizer();
|
|
924
1012
|
var isUri = (uri) => urlSanitizer.isURI(uri);
|
|
@@ -928,7 +1016,8 @@ var isURI = async (uri) => {
|
|
|
928
1016
|
};
|
|
929
1017
|
var sanitizeUrl = (url, opt) => urlSanitizer.sanitize(url, opt ?? {
|
|
930
1018
|
allow: [],
|
|
931
|
-
deny: []
|
|
1019
|
+
deny: [],
|
|
1020
|
+
only: []
|
|
932
1021
|
});
|
|
933
1022
|
var sanitizeURL = async (url, opt) => {
|
|
934
1023
|
const res = await sanitizeUrl(url, opt);
|