url-sanitizer 0.2.2 → 0.3.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.
@@ -0,0 +1,233 @@
1
+ [
2
+ 7,
3
+ 8,
4
+ 9,
5
+ 10,
6
+ 11,
7
+ 12,
8
+ 13,
9
+ 27,
10
+ 32,
11
+ 33,
12
+ 34,
13
+ 35,
14
+ 36,
15
+ 37,
16
+ 38,
17
+ 39,
18
+ 40,
19
+ 41,
20
+ 42,
21
+ 43,
22
+ 44,
23
+ 45,
24
+ 46,
25
+ 47,
26
+ 48,
27
+ 49,
28
+ 50,
29
+ 51,
30
+ 52,
31
+ 53,
32
+ 54,
33
+ 55,
34
+ 56,
35
+ 57,
36
+ 58,
37
+ 59,
38
+ 60,
39
+ 61,
40
+ 62,
41
+ 63,
42
+ 64,
43
+ 65,
44
+ 66,
45
+ 67,
46
+ 68,
47
+ 69,
48
+ 70,
49
+ 71,
50
+ 72,
51
+ 73,
52
+ 74,
53
+ 75,
54
+ 76,
55
+ 77,
56
+ 78,
57
+ 79,
58
+ 80,
59
+ 81,
60
+ 82,
61
+ 83,
62
+ 84,
63
+ 85,
64
+ 86,
65
+ 87,
66
+ 88,
67
+ 89,
68
+ 90,
69
+ 91,
70
+ 92,
71
+ 93,
72
+ 94,
73
+ 95,
74
+ 96,
75
+ 97,
76
+ 98,
77
+ 99,
78
+ 100,
79
+ 101,
80
+ 102,
81
+ 103,
82
+ 104,
83
+ 105,
84
+ 106,
85
+ 107,
86
+ 108,
87
+ 109,
88
+ 110,
89
+ 111,
90
+ 112,
91
+ 113,
92
+ 114,
93
+ 115,
94
+ 116,
95
+ 117,
96
+ 118,
97
+ 119,
98
+ 120,
99
+ 121,
100
+ 122,
101
+ 123,
102
+ 124,
103
+ 125,
104
+ 126,
105
+ 128,
106
+ 129,
107
+ 130,
108
+ 131,
109
+ 132,
110
+ 133,
111
+ 134,
112
+ 135,
113
+ 136,
114
+ 137,
115
+ 138,
116
+ 139,
117
+ 140,
118
+ 141,
119
+ 142,
120
+ 143,
121
+ 144,
122
+ 145,
123
+ 146,
124
+ 147,
125
+ 148,
126
+ 149,
127
+ 150,
128
+ 151,
129
+ 152,
130
+ 153,
131
+ 154,
132
+ 155,
133
+ 156,
134
+ 157,
135
+ 158,
136
+ 159,
137
+ 160,
138
+ 161,
139
+ 162,
140
+ 163,
141
+ 164,
142
+ 165,
143
+ 166,
144
+ 167,
145
+ 168,
146
+ 169,
147
+ 170,
148
+ 171,
149
+ 172,
150
+ 173,
151
+ 174,
152
+ 175,
153
+ 176,
154
+ 177,
155
+ 178,
156
+ 179,
157
+ 180,
158
+ 181,
159
+ 182,
160
+ 183,
161
+ 184,
162
+ 185,
163
+ 186,
164
+ 187,
165
+ 188,
166
+ 189,
167
+ 190,
168
+ 191,
169
+ 192,
170
+ 193,
171
+ 194,
172
+ 195,
173
+ 196,
174
+ 197,
175
+ 198,
176
+ 199,
177
+ 200,
178
+ 201,
179
+ 202,
180
+ 203,
181
+ 204,
182
+ 205,
183
+ 206,
184
+ 207,
185
+ 208,
186
+ 209,
187
+ 210,
188
+ 211,
189
+ 212,
190
+ 213,
191
+ 214,
192
+ 215,
193
+ 216,
194
+ 217,
195
+ 218,
196
+ 219,
197
+ 220,
198
+ 221,
199
+ 222,
200
+ 223,
201
+ 224,
202
+ 225,
203
+ 226,
204
+ 227,
205
+ 228,
206
+ 229,
207
+ 230,
208
+ 231,
209
+ 232,
210
+ 233,
211
+ 234,
212
+ 235,
213
+ 236,
214
+ 237,
215
+ 238,
216
+ 239,
217
+ 240,
218
+ 241,
219
+ 242,
220
+ 243,
221
+ 244,
222
+ 245,
223
+ 246,
224
+ 247,
225
+ 248,
226
+ 249,
227
+ 250,
228
+ 251,
229
+ 252,
230
+ 253,
231
+ 254,
232
+ 255
233
+ ]
@@ -4,6 +4,7 @@
4
4
 
5
5
  /* shared */
6
6
  import { getType, isString } from './common.js';
7
+ import textCharTable from '../lib/file/text-chars.json' assert { type: 'json' };
7
8
  import uriSchemes from '../lib/iana/uri-schemes.json' assert { type: 'json' };
8
9
 
9
10
  /* constants */
@@ -64,7 +65,6 @@ export const escapeUrlEncodedHtmlChars = ch => {
64
65
  /**
65
66
  * parse base64
66
67
  *
67
- * @see {@link https://github.com/file/file/blob/master/src/encoding.c}
68
68
  * @param {string} data - base64 data
69
69
  * @returns {string} - parsed data / base64 data
70
70
  */
@@ -74,13 +74,7 @@ export const parseBase64 = data => {
74
74
  }
75
75
  const bin = atob(data);
76
76
  const uint8arr = Uint8Array.from([...bin].map(c => c.charCodeAt(0)));
77
- const textChars = new Set([0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x1b]);
78
- for (let i = 0x20; i < 0x7f; i++) {
79
- textChars.add(i);
80
- }
81
- for (let i = 0x80; i <= 0xff; i++) {
82
- textChars.add(i);
83
- }
77
+ const textChars = new Set(textCharTable);
84
78
  let parsedData;
85
79
  if (uint8arr.every(c => textChars.has(c))) {
86
80
  parsedData = bin.replace(/\s/g, getUrlEncodedString);
@@ -180,6 +174,17 @@ export class URISchemes {
180
174
 
181
175
  /* URL sanitizer */
182
176
  export class URLSanitizer extends URISchemes {
177
+ /* private fields */
178
+ #recurse;
179
+
180
+ /**
181
+ * construct
182
+ */
183
+ constructor() {
184
+ super();
185
+ this.#recurse = new Map();
186
+ }
187
+
183
188
  /**
184
189
  * sanitize URL
185
190
  * NOTE: `data` and/or `file` schemes must be explicitly allowed
@@ -189,13 +194,12 @@ export class URLSanitizer extends URISchemes {
189
194
  * @param {object} opt - options
190
195
  * @param {Array.<string>} opt.allow - array of allowed schemes
191
196
  * @param {Array.<string>} opt.deny - array of denied schemes
192
- * @param {boolean} opt.escapeTags - escape tags and quotes in data URL
193
197
  * @returns {?string} - sanitized URL
194
198
  */
195
- sanitize(url, opt = { allow: [], deny: [], escapeTags: true }) {
199
+ sanitize(url, opt = { allow: [], deny: [] }) {
196
200
  let sanitizedUrl;
197
201
  if (super.isURI(url)) {
198
- const { allow, deny, escapeTags } = opt ?? {};
202
+ const { allow, deny } = opt ?? {};
199
203
  const { href, pathname, protocol } = new URL(url);
200
204
  const scheme = protocol.replace(/:$/, '');
201
205
  const schemeParts = scheme.split('+');
@@ -247,71 +251,60 @@ export class URLSanitizer extends URISchemes {
247
251
  if (isBase64) {
248
252
  let parsedData = parseBase64(data);
249
253
  if (parsedData !== data) {
250
- const regDataUrl = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/g;
251
- if (regDataUrl.test(parsedData)) {
252
- const dataUrlArr = [];
253
- let arr = regDataUrl.exec(parsedData);
254
- do {
255
- if (arr) {
256
- dataUrlArr.push(arr);
257
- }
258
- } while ((arr = regDataUrl.exec(parsedData)));
259
- if (dataUrlArr.length) {
260
- for (const i of dataUrlArr) {
261
- const [dataUrl] = i;
254
+ if (/data:[^,]*,/.test(parsedData)) {
255
+ const regDataUrl = /data:[^,]*,[^"]+/g;
256
+ const regBase64DataUrl = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/;
257
+ const matchedDataUrls = parsedData.matchAll(regDataUrl);
258
+ const items = [...matchedDataUrls].reverse();
259
+ if (items.length) {
260
+ for (const item of items) {
261
+ const { index } = item;
262
+ let [dataUrl] = item;
263
+ if (regBase64DataUrl.test(dataUrl)) {
264
+ [dataUrl] = regBase64DataUrl.exec(dataUrl);
265
+ }
266
+ const [beforeDataUrl, afterDataUrl] = [
267
+ parsedData.substring(0, index),
268
+ parsedData.substring(index + dataUrl.length)
269
+ ];
270
+ this.#recurse.set(dataUrl, dataUrl);
262
271
  const parsedDataUrl = this.sanitize(dataUrl, {
263
272
  allow: ['data']
264
273
  });
265
274
  if (parsedDataUrl) {
266
- parsedData = parsedData.replace(dataUrl, parsedDataUrl);
275
+ parsedData = [
276
+ beforeDataUrl,
277
+ parsedDataUrl,
278
+ afterDataUrl
279
+ ].join('');
267
280
  }
268
- type = 0;
269
281
  }
270
282
  }
271
- } else if (/data:[^,]*,/.test(parsedData) &&
272
- !(escapeTags ?? true)) {
273
- const dataArr = parsedData.split(/data:[^,]*,/);
274
- const l = dataArr.length;
275
- let i = 1;
276
- while (i < l) {
277
- const dataItem = dataArr[i]
278
- .replace(regEncodedChars, escapeUrlEncodedHtmlChars);
279
- parsedData = parsedData.replace(dataArr[i], dataItem);
280
- i++;
281
- }
282
- type = 0;
283
283
  }
284
- urlToSanitize = `${scheme}:${mediaType.join(';')},${parsedData}`;
285
- if ((escapeTags ?? true)) {
284
+ if (this.#recurse.has(url)) {
285
+ this.#recurse.delete(url);
286
+ } else {
286
287
  type = 1;
287
- } else if (!Number.isInteger(type)) {
288
- type = 2;
289
288
  }
290
- } else if ((escapeTags ?? true)) {
291
- type = 1;
289
+ urlToSanitize = `${scheme}:${mediaType.join(';')},${parsedData}`;
292
290
  } else {
293
- type = 2;
291
+ type = 1;
294
292
  }
295
- } else if ((escapeTags ?? true)) {
296
- type = 1;
293
+ } else if (this.#recurse.has(url)) {
294
+ this.#recurse.delete(url);
297
295
  } else {
298
- type = 2;
296
+ type = 1;
299
297
  }
300
298
  } else {
301
299
  type = 1;
302
300
  }
303
- switch (type) {
304
- case 1:
305
- sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString)
306
- .replace(regAmp, escapeUrlEncodedHtmlChars)
307
- .replace(regEncodedChars, escapeUrlEncodedHtmlChars);
308
- break;
309
- case 2:
310
- sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString)
311
- .replace(regAmp, escapeUrlEncodedHtmlChars);
312
- break;
313
- default:
314
- sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString);
301
+ if (type === 1) {
302
+ sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString)
303
+ .replace(regAmp, escapeUrlEncodedHtmlChars)
304
+ .replace(regEncodedChars, escapeUrlEncodedHtmlChars);
305
+ } else {
306
+ sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString)
307
+ .replace(regAmp, escapeUrlEncodedHtmlChars);
315
308
  }
316
309
  }
317
310
  }
@@ -350,8 +343,7 @@ export const isURI = async uri => {
350
343
  */
351
344
  const sanitizeUrl = (url, opt) => urlSanitizer.sanitize(url, opt ?? {
352
345
  allow: [],
353
- deny: [],
354
- escapeTags: true
346
+ deny: []
355
347
  });
356
348
 
357
349
  /**
@@ -359,7 +351,7 @@ const sanitizeUrl = (url, opt) => urlSanitizer.sanitize(url, opt ?? {
359
351
  *
360
352
  * @param {string} url - URL input
361
353
  * @param {object} opt - options
362
- * @returns {Promise<?string>} - sanitized URL
354
+ * @returns {Promise.<?string>} - sanitized URL
363
355
  */
364
356
  export const sanitizeURL = async (url, opt) => {
365
357
  const res = await sanitizeUrl(url, opt);