url-sanitizer 2.2.0 → 2.3.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/package.json CHANGED
@@ -29,6 +29,7 @@
29
29
  "jsdom": "^29.1.1"
30
30
  },
31
31
  "devDependencies": {
32
+ "@braintree/sanitize-url": "^7.1.2",
32
33
  "@types/jsdom": "^28.0.3",
33
34
  "@types/node": "^25.8.0",
34
35
  "c8": "^11.0.0",
@@ -40,13 +41,16 @@
40
41
  "eslint-plugin-jsdoc": "^62.9.0",
41
42
  "eslint-plugin-regexp": "^3.1.0",
42
43
  "eslint-plugin-unicorn": "^64.0.0",
44
+ "fast-check": "^4.8.0",
43
45
  "globals": "^17.6.0",
44
46
  "http-server": "^14.1.1",
47
+ "mitata": "^1.0.34",
45
48
  "mocha": "^11.7.5",
46
49
  "neostandard": "^0.13.0",
47
50
  "npm-run-all2": "^8.0.4",
48
51
  "papaparse": "^5.5.3",
49
52
  "sinon": "^22.0.0",
53
+ "strict-url-sanitise": "^0.0.1",
50
54
  "typescript": "^6.0.3",
51
55
  "undici": "^8.0.2"
52
56
  },
@@ -63,6 +67,7 @@
63
67
  "serialize-javascript": "^7.0.4"
64
68
  },
65
69
  "scripts": {
70
+ "bench": "node benchmark/benchmark.js",
66
71
  "build": "npm run include && npm run tsc && npm run lint && npm test && npm run copy && npm run bundle",
67
72
  "bundle": "npm-run-all -s bundle:*",
68
73
  "bundle:min": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer.min.js --bundle --minify --sourcemap bundle/index.js",
@@ -79,5 +84,5 @@
79
84
  "test:bdd": "http-server ./ -c-1 -o test/bdd/url-sanitizer.test.html",
80
85
  "tsc": "node scripts/index clean --dir=types -i && npx tsc"
81
86
  },
82
- "version": "2.2.0"
87
+ "version": "2.3.1"
83
88
  }
@@ -84,9 +84,15 @@ class URLSanitizer extends URISchemes {
84
84
  }
85
85
  this.#nest++;
86
86
  this.#recurse.add(dataUrl);
87
- const parsedDataUrl = this.sanitize(dataUrl, {
88
- allow: ['data']
89
- });
87
+ let parsedDataUrl;
88
+ try {
89
+ parsedDataUrl = this.sanitize(dataUrl, {
90
+ allow: ['data']
91
+ });
92
+ } finally {
93
+ this.#nest--;
94
+ this.#recurse.delete(dataUrl);
95
+ }
90
96
  const { index } = item;
91
97
  const [preDataUrl, postDataUrl] = [
92
98
  replacedData.substring(0, index),
@@ -111,13 +117,26 @@ class URLSanitizer extends URISchemes {
111
117
  if (!isString(dom)) {
112
118
  throw new TypeError(`Expected String but got ${getType(dom)}.`);
113
119
  }
114
- let purifiedDom = domPurify.sanitize(decodeURIComponent(dom));
120
+ let decodedDom = dom;
121
+ try {
122
+ // prevent URIError caused by malformed percent-encoding
123
+ // e.g., a standalone '%'
124
+ decodedDom = decodeURIComponent(dom);
125
+ } catch (e) {
126
+ // fall through
127
+ }
128
+ let purifiedDom = domPurify.sanitize(decodedDom);
115
129
  if (purifiedDom && REG_DATA_URL.test(purifiedDom)) {
116
130
  purifiedDom = this.replace(purifiedDom);
117
131
  }
118
132
  purifiedDom = purifiedDom.replace(/(?:#|%23)$/, '')
119
133
  .replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/, '');
120
- return encodeURI(purifiedDom);
134
+ try {
135
+ // prevent URIError caused by lone surrogates
136
+ return encodeURI(purifiedDom);
137
+ } catch (e) {
138
+ return purifiedDom;
139
+ }
121
140
  }
122
141
 
123
142
  /**
@@ -130,19 +149,22 @@ class URLSanitizer extends URISchemes {
130
149
  * @param {Array.<string>} [opt.allow] - array of allowed schemes
131
150
  * @param {Array.<string>} [opt.deny] - array of denied schemes
132
151
  * @param {Array.<string>} [opt.only] - array of specific schemes to allow
152
+ * @param {boolean} [opt.allowRelative] - allow relative URLs
153
+ * @param {boolean} [opt.debug] - enable debug mode
133
154
  * @returns {?string} - sanitized URL
134
155
  */
135
156
  sanitize(url, opt) {
136
157
  if (this.#nest > HEX) {
137
- this.#nest = 0;
138
158
  throw new Error('Data URLs nested too deeply.');
139
159
  }
140
- const { allow, deny, only, debug = false } = opt ?? {};
160
+ const {
161
+ allow, deny, only, allowRelative = false, debug = false
162
+ } = opt ?? {};
141
163
  const schemeMap = new Map([
142
164
  ['blob', false],
143
165
  ['data', false],
144
166
  ['file', false],
145
- ['javascrpt', false],
167
+ ['javascript', false],
146
168
  ['vbscript', false]
147
169
  ]);
148
170
  const tempScheme = new Set();
@@ -215,25 +237,46 @@ class URLSanitizer extends URISchemes {
215
237
  }
216
238
  }
217
239
  let sanitizedUrl;
218
- if (super.verify(url)) {
219
- const { hash, href, pathname, protocol, search } = new URL(url);
220
- const scheme = protocol.replace(/:$/, '');
221
- const schemeParts = scheme.split('+');
222
- let bool;
223
- if (restrictScheme) {
224
- bool = schemeParts.every(s => schemeMap.get(s));
240
+ let isVerified = super.verify(url);
241
+ let isRelative = false;
242
+ if (!isVerified && allowRelative) {
243
+ try {
244
+ const { hostname, protocol } = new URL(url, 'http://dummy.local');
245
+ if (protocol === 'http:' && hostname === 'dummy.local') {
246
+ isVerified = true;
247
+ isRelative = true;
248
+ }
249
+ } catch (e) {
250
+ logDebug(debug, 'Failed to parse relative URL.', e);
251
+ }
252
+ }
253
+ if (isVerified) {
254
+ let hash, href, pathname, protocol, search;
255
+ let scheme = '';
256
+ let schemeParts = [];
257
+ let bool = false;
258
+ if (isRelative) {
259
+ bool = true;
225
260
  } else {
226
- for (const [key, value] of schemeMap.entries()) {
227
- bool = value || (scheme !== key && schemeParts.every(s => s !== key));
228
- if (!bool) {
229
- break;
261
+ const urlObj = new URL(url);
262
+ ({ hash, href, pathname, protocol, search } = urlObj);
263
+ scheme = protocol.replace(/:$/, '');
264
+ schemeParts = scheme.split('+');
265
+ if (restrictScheme) {
266
+ bool = schemeParts.every(s => schemeMap.get(s));
267
+ } else {
268
+ for (const [key, value] of schemeMap.entries()) {
269
+ bool =
270
+ value || (scheme !== key && schemeParts.every(s => s !== key));
271
+ if (!bool) {
272
+ break;
273
+ }
230
274
  }
231
275
  }
232
276
  }
233
277
  if (bool) {
234
- const isDataUrl = schemeParts.includes('data');
235
- let finalize;
236
- let urlToSanitize = href;
278
+ const isDataUrl = isRelative ? false : schemeParts.includes('data');
279
+ let urlToSanitize = isRelative ? url : href;
237
280
  if (isDataUrl) {
238
281
  const [mediaType, ...dataParts] = pathname.split(',');
239
282
  const data = `${dataParts.join(',')}${search}${hash}`;
@@ -258,15 +301,7 @@ class URLSanitizer extends URISchemes {
258
301
  if (parsedData !== data || containsDataUrl) {
259
302
  if (containsDataUrl) {
260
303
  parsedData = this.replace(parsedData);
261
- } else if (this.#recurse.has(url)) {
262
- this.#recurse.delete(url);
263
- } else {
264
- finalize = true;
265
304
  }
266
- } else if (this.#recurse.has(url)) {
267
- this.#recurse.delete(url);
268
- } else {
269
- finalize = true;
270
305
  }
271
306
  if (!mediaType || REG_MIME_DOM.test(mediaType)) {
272
307
  parsedData = this.purify(parsedData);
@@ -279,8 +314,6 @@ class URLSanitizer extends URISchemes {
279
314
  } else {
280
315
  urlToSanitize = '';
281
316
  }
282
- } else {
283
- finalize = true;
284
317
  }
285
318
  if (!isDataUrl && REG_TAG_QUOT.test(urlToSanitize)) {
286
319
  const item = REG_TAG_QUOT.exec(urlToSanitize);
@@ -291,11 +324,6 @@ class URLSanitizer extends URISchemes {
291
324
  if (urlToSanitize) {
292
325
  sanitizedUrl =
293
326
  urlToSanitize.replace(/%26/g, escapeURLEncodedHTMLChars);
294
- if (finalize) {
295
- this.#nest = 0;
296
- }
297
- } else {
298
- this.#nest = 0;
299
327
  }
300
328
  }
301
329
  if (tempScheme.size) {
@@ -399,6 +427,7 @@ export const sanitizeURL = async (url, opt = {
399
427
  allow: [],
400
428
  deny: [],
401
429
  only: [],
430
+ allowRelative: false,
402
431
  debug: false,
403
432
  maxBlobSize: MAX_BLOB_SIZE
404
433
  }) => {
@@ -447,7 +476,7 @@ export const sanitizeURL = async (url, opt = {
447
476
  }
448
477
  }
449
478
  URL.revokeObjectURL(url);
450
- } else if (scheme) {
479
+ } else if (scheme || opt.allowRelative) {
451
480
  res = urlSanitizer.sanitize(url, opt);
452
481
  }
453
482
  }
@@ -478,7 +507,7 @@ export const sanitizeURLSync = (url, opt) => {
478
507
  }
479
508
  if (scheme === 'blob') {
480
509
  URL.revokeObjectURL(url);
481
- } else if (scheme) {
510
+ } else if (scheme || opt?.allowRelative) {
482
511
  res = urlSanitizer.sanitize(url, opt);
483
512
  }
484
513
  }
@@ -41,6 +41,8 @@ export class URLSanitizer extends URISchemes {
41
41
  allow?: string[] | undefined;
42
42
  deny?: string[] | undefined;
43
43
  only?: string[] | undefined;
44
+ allowRelative?: boolean | undefined;
45
+ debug?: boolean | undefined;
44
46
  }): string | null;
45
47
  parse(url: string, opt?: {
46
48
  allow?: string[] | undefined;