url-sanitizer 2.2.0 → 2.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.
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.0"
83
88
  }
@@ -111,13 +111,26 @@ class URLSanitizer extends URISchemes {
111
111
  if (!isString(dom)) {
112
112
  throw new TypeError(`Expected String but got ${getType(dom)}.`);
113
113
  }
114
- let purifiedDom = domPurify.sanitize(decodeURIComponent(dom));
114
+ let decodedDom = dom;
115
+ try {
116
+ // prevent URIError caused by malformed percent-encoding
117
+ // e.g., a standalone '%'
118
+ decodedDom = decodeURIComponent(dom);
119
+ } catch (e) {
120
+ // fall through
121
+ }
122
+ let purifiedDom = domPurify.sanitize(decodedDom);
115
123
  if (purifiedDom && REG_DATA_URL.test(purifiedDom)) {
116
124
  purifiedDom = this.replace(purifiedDom);
117
125
  }
118
126
  purifiedDom = purifiedDom.replace(/(?:#|%23)$/, '')
119
127
  .replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/, '');
120
- return encodeURI(purifiedDom);
128
+ try {
129
+ // prevent URIError caused by lone surrogates
130
+ return encodeURI(purifiedDom);
131
+ } catch (e) {
132
+ return purifiedDom;
133
+ }
121
134
  }
122
135
 
123
136
  /**
@@ -130,6 +143,8 @@ class URLSanitizer extends URISchemes {
130
143
  * @param {Array.<string>} [opt.allow] - array of allowed schemes
131
144
  * @param {Array.<string>} [opt.deny] - array of denied schemes
132
145
  * @param {Array.<string>} [opt.only] - array of specific schemes to allow
146
+ * @param {boolean} [opt.allowRelative] - allow relative URLs
147
+ * @param {boolean} [opt.debug] - enable debug mode
133
148
  * @returns {?string} - sanitized URL
134
149
  */
135
150
  sanitize(url, opt) {
@@ -137,7 +152,9 @@ class URLSanitizer extends URISchemes {
137
152
  this.#nest = 0;
138
153
  throw new Error('Data URLs nested too deeply.');
139
154
  }
140
- const { allow, deny, only, debug = false } = opt ?? {};
155
+ const {
156
+ allow, deny, only, allowRelative = false, debug = false
157
+ } = opt ?? {};
141
158
  const schemeMap = new Map([
142
159
  ['blob', false],
143
160
  ['data', false],
@@ -215,25 +232,47 @@ class URLSanitizer extends URISchemes {
215
232
  }
216
233
  }
217
234
  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));
235
+ let isVerified = super.verify(url);
236
+ let isRelative = false;
237
+ if (!isVerified && allowRelative) {
238
+ try {
239
+ const { hostname, protocol } = new URL(url, 'http://dummy.local');
240
+ if (protocol === 'http:' && hostname === 'dummy.local') {
241
+ isVerified = true;
242
+ isRelative = true;
243
+ }
244
+ } catch (e) {
245
+ logDebug(debug, 'Failed to parse relative URL.', e);
246
+ }
247
+ }
248
+ if (isVerified) {
249
+ let hash, href, pathname, protocol, search;
250
+ let scheme = '';
251
+ let schemeParts = [];
252
+ let bool = false;
253
+ if (isRelative) {
254
+ bool = true;
225
255
  } else {
226
- for (const [key, value] of schemeMap.entries()) {
227
- bool = value || (scheme !== key && schemeParts.every(s => s !== key));
228
- if (!bool) {
229
- break;
256
+ const urlObj = new URL(url);
257
+ ({ hash, href, pathname, protocol, search } = urlObj);
258
+ scheme = protocol.replace(/:$/, '');
259
+ schemeParts = scheme.split('+');
260
+ if (restrictScheme) {
261
+ bool = schemeParts.every(s => schemeMap.get(s));
262
+ } else {
263
+ for (const [key, value] of schemeMap.entries()) {
264
+ bool =
265
+ value || (scheme !== key && schemeParts.every(s => s !== key));
266
+ if (!bool) {
267
+ break;
268
+ }
230
269
  }
231
270
  }
232
271
  }
233
272
  if (bool) {
234
- const isDataUrl = schemeParts.includes('data');
273
+ const isDataUrl = isRelative ? false : schemeParts.includes('data');
235
274
  let finalize;
236
- let urlToSanitize = href;
275
+ let urlToSanitize = isRelative ? url : href;
237
276
  if (isDataUrl) {
238
277
  const [mediaType, ...dataParts] = pathname.split(',');
239
278
  const data = `${dataParts.join(',')}${search}${hash}`;
@@ -399,6 +438,7 @@ export const sanitizeURL = async (url, opt = {
399
438
  allow: [],
400
439
  deny: [],
401
440
  only: [],
441
+ allowRelative: false,
402
442
  debug: false,
403
443
  maxBlobSize: MAX_BLOB_SIZE
404
444
  }) => {
@@ -447,7 +487,7 @@ export const sanitizeURL = async (url, opt = {
447
487
  }
448
488
  }
449
489
  URL.revokeObjectURL(url);
450
- } else if (scheme) {
490
+ } else if (scheme || opt.allowRelative) {
451
491
  res = urlSanitizer.sanitize(url, opt);
452
492
  }
453
493
  }
@@ -478,7 +518,7 @@ export const sanitizeURLSync = (url, opt) => {
478
518
  }
479
519
  if (scheme === 'blob') {
480
520
  URL.revokeObjectURL(url);
481
- } else if (scheme) {
521
+ } else if (scheme || opt?.allowRelative) {
482
522
  res = urlSanitizer.sanitize(url, opt);
483
523
  }
484
524
  }
@@ -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;