url-sanitizer 5.2.0 → 5.2.2

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
@@ -31,7 +31,7 @@
31
31
  "devDependencies": {
32
32
  "@braintree/sanitize-url": "^7.1.2",
33
33
  "@types/jsdom": "^30.0.0",
34
- "@types/node": "^26.3.0",
34
+ "@types/node": "^26.4.1",
35
35
  "c8": "^12.0.0",
36
36
  "chai": "^6.2.2",
37
37
  "commander": "^15.0.0",
@@ -39,22 +39,22 @@
39
39
  "esbuild": "^0.28.2",
40
40
  "eslint": "^10.9.1",
41
41
  "eslint-config-prettier": "^10.1.8",
42
- "eslint-plugin-jsdoc": "^64.2.1",
42
+ "eslint-plugin-jsdoc": "^64.3.5",
43
43
  "eslint-plugin-prettier": "^5.5.6",
44
- "eslint-plugin-regexp": "^3.2.0",
45
- "eslint-plugin-unicorn": "^73.0.0",
44
+ "eslint-plugin-regexp": "^3.3.0",
45
+ "eslint-plugin-unicorn": "^74.0.0",
46
46
  "fast-check": "^4.9.0",
47
- "globals": "^17.11.0",
47
+ "globals": "^17.12.0",
48
48
  "http-server": "^14.1.1",
49
49
  "mitata": "^1.0.34",
50
- "mocha": "^11.8.0",
50
+ "mocha": "^12.0.0",
51
51
  "neostandard": "^0.14.0-next.1",
52
52
  "papaparse": "^5.7.0",
53
53
  "prettier": "^3.9.6",
54
54
  "sinon": "^22.1.0",
55
55
  "strict-url-sanitise": "^0.0.1",
56
56
  "typescript": "^7.0.2",
57
- "undici": "^8.10.0"
57
+ "undici": "^8.10.1"
58
58
  },
59
59
  "overrides": {
60
60
  "mocha": {
@@ -87,5 +87,5 @@
87
87
  "engines": {
88
88
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
89
89
  },
90
- "version": "5.2.0"
90
+ "version": "5.2.2"
91
91
  }
package/src/index.js CHANGED
@@ -8,8 +8,48 @@
8
8
 
9
9
  import { URLSanitizer } from './mjs/sanitize.js';
10
10
 
11
+ /* typedef */
11
12
  /**
12
- * @typedef {import('./mjs/sanitize.js').InspectedURLResult} InspectedURLResult
13
+ * The parsed result of a Data URL.
14
+ * @typedef {object} InspectedDataURL
15
+ * @property {boolean} base64 - Indicates whether the data is base64-encoded.
16
+ * @property {string} data - The actual data part of the Data URL.
17
+ * @property {string} mime - The MIME type of the data.
18
+ */
19
+
20
+ /**
21
+ * The result of an inspected URL, extending the standard URL API.
22
+ * The properties except for input and valid are omitted for invalid URLs.
23
+ * @typedef {object} InspectedURLResult
24
+ * @property {string} input - The original URL input.
25
+ * @property {boolean} valid - Indicates whether the sanitized URL (href) is a valid absolute URL.
26
+ * @property {string|null} href - The sanitized URL, or null.
27
+ * @property {boolean} [relative] - Indicates whether the sanitized URL (href) is a valid relative URL.
28
+ * @property {string} [reason] - The reason why the URL is invalid.
29
+ * @property {InspectedDataURL|null} [data] - The parsed result of a Data URL, or null.
30
+ * @property {string} [origin] - The scheme, domain, and port.
31
+ * @property {string} [protocol] - The protocol scheme.
32
+ * @property {string} [username] - The specified username.
33
+ * @property {string} [password] - The specified password.
34
+ * @property {string} [host] - The domain and port.
35
+ * @property {string} [hostname] - The domain.
36
+ * @property {string} [port] - The port number.
37
+ * @property {string} [pathname] - The path.
38
+ * @property {string} [search] - The query string.
39
+ * @property {string} [hash] - The fragment identifier.
40
+ */
41
+
42
+ /**
43
+ * The sanitization options.
44
+ * @typedef {object} SanitizeOptions
45
+ * @property {string[]} [allow] - The array of schemes to allow.
46
+ * @property {string[]} [deny] - The array of schemes to deny. Takes precedence over `allow`.
47
+ * @property {string[]} [only] - The array of specific schemes to allow. Takes precedence over `allow` and `deny`.
48
+ * @property {boolean} [allowRelative] - Allow relative URLs.
49
+ * @property {boolean} [debug] - Enable debug mode.
50
+ * @property {boolean} [revokeObjectURL] - Revokes the Blob URL after sanitization.
51
+ * @property {number} [maxBlobSize] - The maximum allowed Blob size in bytes.
52
+ * @property {number} [maxLength] - The maximum allowed URL length.
13
53
  */
14
54
 
15
55
  export { URLSanitizer };
@@ -23,15 +63,7 @@ const sanitizer = new URLSanitizer();
23
63
  * NOTE: `blob`, `data`, and `file` schemes must be explicitly allowed.
24
64
  * Given a `blob` URL, it securely converts and returns a sanitized `data` URL.
25
65
  * @param {string} url - URL.
26
- * @param {object} [opt] - options.
27
- * @param {string[]} [opt.allow] - The array of schemes to allow.
28
- * @param {string[]} [opt.deny] - The array of schemes to deny.
29
- * @param {string[]} [opt.only] - The array of specific schemes to allow.
30
- * @param {boolean} [opt.allowRelative] - Allow relative URLs.
31
- * @param {boolean} [opt.debug] - Enable debug mode.
32
- * @param {boolean} [opt.revokeObjectURL] - Revokes the Blob URL after sanitization.
33
- * @param {number} [opt.maxBlobSize] - The maximum allowed Blob size in bytes.
34
- * @param {number} [opt.maxLength] - The maximum allowed URL length.
66
+ * @param {SanitizeOptions} [opt] - The sanitization options.
35
67
  * @returns {Promise<string|null>} A promise resolving to the sanitized URL, or null.
36
68
  */
37
69
  export const sanitizeURL = async (url, opt) => sanitizer.sanitizeURL(url, opt);
@@ -41,14 +73,7 @@ export const sanitizeURL = async (url, opt) => sanitizer.sanitizeURL(url, opt);
41
73
  * NOTE: `data` and `file` schemes must be explicitly allowed.
42
74
  * The `blob` scheme is not supported and will return `null`.
43
75
  * @param {string} url - URL.
44
- * @param {object} [opt] - options.
45
- * @param {string[]} [opt.allow] - The array of schemes to allow.
46
- * @param {string[]} [opt.deny] - The array of schemes to deny.
47
- * @param {string[]} [opt.only] - The array of specific schemes to allow.
48
- * @param {boolean} [opt.allowRelative] - Allow relative URLs.
49
- * @param {boolean} [opt.debug] - Enable debug mode.
50
- * @param {boolean} [opt.revokeObjectURL] - Revokes the Blob URL.
51
- * @param {number} [opt.maxLength] - The maximum allowed URL length.
76
+ * @param {SanitizeOptions} [opt] - The sanitization options.
52
77
  * @returns {string|null} The sanitized URL, or null if denied.
53
78
  */
54
79
  export const sanitizeURLSync = (url, opt) =>
@@ -69,3 +69,19 @@ export const TYPE_TO = -1;
69
69
  * @type {string}
70
70
  */
71
71
  export const DUMMY_BASE = 'http://dummy.local';
72
+
73
+ /**
74
+ * The default sanitization options.
75
+ * @type {import('./sanitize.js').InternalSanitizeOptions}
76
+ */
77
+ export const DEFAULT_OPTS = {
78
+ allow: Object.freeze([]),
79
+ deny: Object.freeze([]),
80
+ only: Object.freeze([]),
81
+ allowRelative: false,
82
+ debug: false,
83
+ revokeObjectURL: false,
84
+ maxBlobSize: MAX_BLOB_SIZE,
85
+ maxLength: MAX_URL_LENGTH,
86
+ schemes: new Set()
87
+ };
@@ -2,7 +2,7 @@
2
2
  * dompurify.js
3
3
  */
4
4
 
5
- import DOMPurify from 'dompurify';
5
+ import createDOMPurify from 'dompurify';
6
6
  import { JSDOM } from 'jsdom';
7
7
 
8
8
  const { window } = new JSDOM('');
@@ -12,4 +12,4 @@ const { window } = new JSDOM('');
12
12
  * Used to sanitize HTML/SVG strings.
13
13
  * @type {import('dompurify').DOMPurify}
14
14
  */
15
- export const domPurify = DOMPurify(window);
15
+ export const domPurify = createDOMPurify(window);
package/src/mjs/filter.js CHANGED
@@ -18,8 +18,17 @@ import {
18
18
  truncateURL
19
19
  } from './utility.js';
20
20
 
21
+ /* typedef */
22
+ /**
23
+ * @typedef {import('dompurify').DOMPurify} DOMPurify
24
+ * @typedef {import('../index.js').InspectedDataURL} InspectedDataURL
25
+ * @typedef {import('../index.js').InspectedURLResult} InspectedURLResult
26
+ * @typedef {import('../index.js').SanitizeOptions} SanitizeOptions
27
+ * @typedef {import('./sanitize.js').InternalSanitizeOptions} InternalSanitizeOptions
28
+ */
29
+
21
30
  /* constants */
22
- import { DUMMY_BASE, MAX_NEST } from './constant.js';
31
+ import { DEFAULT_OPTS, DUMMY_BASE, MAX_NEST } from './constant.js';
23
32
  import {
24
33
  REG_AMP_ENC,
25
34
  REG_MIME_DOM,
@@ -35,10 +44,10 @@ import {
35
44
  */
36
45
  export class SanitizeContext {
37
46
  /**
38
- * @param {object} opt - Sanitization options.
39
- * @param {object} domPurifyInstance - The DOMPurify instance.
47
+ * @param {DOMPurify} domPurifyInstance - The DOMPurify instance.
48
+ * @param {InternalSanitizeOptions} opt - The internal sanitization options.
40
49
  */
41
- constructor(opt, domPurifyInstance) {
50
+ constructor(domPurifyInstance, opt = DEFAULT_OPTS) {
42
51
  this.domPurify = domPurifyInstance;
43
52
  this.nest = 0;
44
53
  this.recurse = new Set();
@@ -56,20 +65,18 @@ export class SanitizeContext {
56
65
  /**
57
66
  * Compiles allow, deny, and only rules into the context.
58
67
  * @private
59
- * @param {object} opt - Sanitization options.
68
+ * @param {InternalSanitizeOptions} opt - The internal sanitization options.
60
69
  */
61
- #compileRules(opt = {}) {
62
- const { allowRelative, debug, schemes, allow, deny, only } = opt;
70
+ #compileRules(opt) {
71
+ const { allowRelative, debug, allow, deny, only } = opt;
63
72
  this.allowRelative = !!allowRelative;
64
73
  this.debug = !!debug;
65
74
  if (Array.isArray(only) && only.length) {
66
- this.schemes = new Set();
67
75
  this.restrictScheme = true;
68
76
  for (const scheme of only) {
69
77
  this.#registerScheme(scheme);
70
78
  }
71
79
  } else {
72
- this.schemes = new Set(schemes || []);
73
80
  if (Array.isArray(allow) && allow.length) {
74
81
  for (const scheme of allow) {
75
82
  this.#registerScheme(scheme);
@@ -110,7 +117,6 @@ export class SanitizeContext {
110
117
  }
111
118
  }
112
119
  this.schemeMap.set(normalizedScheme, true);
113
- this.schemes.add(normalizedScheme);
114
120
  return true;
115
121
  }
116
122
 
@@ -149,10 +155,10 @@ export class SanitizeFilter {
149
155
  #defaultOpts;
150
156
 
151
157
  /**
152
- * @param {object} options - The default sanitization options.
158
+ * @param {InternalSanitizeOptions} opt - The default sanitization options.
153
159
  */
154
- constructor(options) {
155
- this.#defaultOpts = options;
160
+ constructor(opt = DEFAULT_OPTS) {
161
+ this.#defaultOpts = opt;
156
162
  }
157
163
 
158
164
  /**
@@ -257,29 +263,30 @@ export class SanitizeFilter {
257
263
  * @private
258
264
  * @param {string} dom - The URL-encoded DOM string.
259
265
  * @param {SanitizeContext} ctx - The sanitization context.
260
- * @returns {string} The purified DOM string.
266
+ * @returns {string|null} The purified DOM string or null.
261
267
  */
262
268
  #purify(dom, ctx) {
263
- let decodedDom = dom;
269
+ let decodedDom = '';
264
270
  try {
265
271
  decodedDom = decodeURIComponent(dom);
266
272
  } catch {
267
- // fall through
273
+ decodedDom = dom;
268
274
  }
269
- let purifiedDom;
275
+ const hook = (node, evt) => this.#handleSanitizeAttribute(node, evt, ctx);
276
+ let purifiedDom = '';
270
277
  try {
271
- ctx.domPurify.addHook('uponSanitizeAttribute', (node, evt) =>
272
- this.#handleSanitizeAttribute(node, evt, ctx)
273
- );
278
+ ctx.domPurify.addHook('uponSanitizeAttribute', hook);
274
279
  purifiedDom = ctx.domPurify.sanitize(decodedDom);
275
280
  } finally {
276
- ctx.domPurify.removeHook('uponSanitizeAttribute');
281
+ ctx.domPurify.removeHook('uponSanitizeAttribute', hook);
282
+ }
283
+ if (!purifiedDom) {
284
+ return null;
277
285
  }
278
- purifiedDom = trimTrailingEmptyQueryAndHash(purifiedDom);
279
286
  try {
280
- return encodeURI(purifiedDom);
287
+ return encodeURI(trimTrailingEmptyQueryAndHash(purifiedDom));
281
288
  } catch {
282
- return purifiedDom;
289
+ return null;
283
290
  }
284
291
  }
285
292
 
@@ -346,14 +353,14 @@ export class SanitizeFilter {
346
353
  /**
347
354
  * Sanitizes the URL.
348
355
  * @param {string} url - The URL string to sanitize.
349
- * @param {object} options - Sanitization options.
356
+ * @param {InternalSanitizeOptions} opt - The internal sanitization options.
350
357
  * @returns {string|null} The sanitized URL, or null.
351
358
  */
352
- sanitize(url, options = this.#defaultOpts) {
359
+ sanitize(url, opt = this.#defaultOpts) {
353
360
  if (!url || !isString(url)) {
354
361
  return null;
355
362
  }
356
- const { deny, maxLength, only, allowRelative } = options;
363
+ const { deny, maxLength, only, allowRelative } = opt;
357
364
  if (Number.isInteger(maxLength) && url.length > maxLength) {
358
365
  const msg = `URL length ${url.length} exceeds max length ${maxLength}.`;
359
366
  throw new RangeError(msg);
@@ -365,17 +372,17 @@ export class SanitizeFilter {
365
372
  // Early return for standard HTTP/HTTPS URLs without restrictive rules.
366
373
  if (
367
374
  !hasRestrictiveRules &&
368
- (url.startsWith('https://') || url.startsWith('http://')) &&
375
+ /^https?:\/\//.test(url) &&
369
376
  !REG_TAG_QUOT.test(url) &&
370
377
  !url.includes('data:')
371
378
  ) {
372
379
  const urlObj = parseURL(url, null, true);
373
- if (urlObj && options.schemes.has(urlObj.protocol.replace(/:$/, ''))) {
380
+ if (urlObj && opt.schemes.has(urlObj.protocol.replace(/:$/, ''))) {
374
381
  return urlObj.href.replace(/%26/g, escapeURLEncodedHTMLChars);
375
382
  }
376
383
  return null;
377
384
  }
378
- const ctx = new SanitizeContext(options, domPurify);
385
+ const ctx = new SanitizeContext(domPurify, opt);
379
386
  return this.#process(url, ctx);
380
387
  }
381
388
 
@@ -383,24 +390,22 @@ export class SanitizeFilter {
383
390
  * Asynchronously sanitizes an ArrayBuffer and converts it to a Data URL.
384
391
  * @param {ArrayBuffer} buffer - The target buffer.
385
392
  * @param {string} mimeType - The MIME type of the buffer.
386
- * @param {object} options - Sanitization options.
393
+ * @param {InternalSanitizeOptions} opt - The internal sanitization options.
387
394
  * @returns {Promise<string|null>} The sanitized Data URL, or null.
388
395
  */
389
- async sanitizeBuffer(buffer, mimeType, options = this.#defaultOpts) {
396
+ async sanitizeBuffer(buffer, mimeType, opt = this.#defaultOpts) {
390
397
  if (!(buffer instanceof ArrayBuffer)) {
391
398
  return null;
392
399
  }
393
400
  const base64Data = encodeBufferToBase64(buffer);
394
- const dataUrl = `data:${mimeType ? `${mimeType};base64` : 'base64'},${base64Data}`;
395
- if (
396
- Number.isInteger(options.maxLength) &&
397
- dataUrl.length > options.maxLength
398
- ) {
401
+ const mime = mimeType ? `${mimeType};base64` : 'base64';
402
+ const dataUrl = `data:${mime},${base64Data}`;
403
+ if (Number.isInteger(opt.maxLength) && dataUrl.length > opt.maxLength) {
399
404
  throw new RangeError(
400
- `URL length ${dataUrl.length} exceeds max length ${options.maxLength}.`
405
+ `URL length ${dataUrl.length} exceeds max length ${opt.maxLength}.`
401
406
  );
402
407
  }
403
- const ctx = new SanitizeContext(options, domPurify);
408
+ const ctx = new SanitizeContext(domPurify, opt);
404
409
  if (!ctx.schemeMap.get('data')) {
405
410
  return null;
406
411
  }
@@ -414,14 +419,14 @@ export class SanitizeFilter {
414
419
  /**
415
420
  * Asynchronously sanitizes the Data URL.
416
421
  * @param {string} url - The URL string to sanitize.
417
- * @param {object} options - Sanitization options.
422
+ * @param {InternalSanitizeOptions} opt - The internal sanitization options.
418
423
  * @returns {Promise<string|null>} The sanitized Data URL, or null.
419
424
  */
420
- async sanitizeDataURL(url, options = this.#defaultOpts) {
425
+ async sanitizeDataURL(url, opt = this.#defaultOpts) {
421
426
  if (!url || !isString(url)) {
422
427
  return null;
423
428
  }
424
- const { maxLength } = options;
429
+ const { maxLength } = opt;
425
430
  if (Number.isInteger(maxLength) && url.length > maxLength) {
426
431
  throw new RangeError(
427
432
  `URL length ${url.length} exceeds max length ${maxLength}.`
@@ -435,7 +440,7 @@ export class SanitizeFilter {
435
440
  if (scheme !== 'data') {
436
441
  return null;
437
442
  }
438
- const ctx = new SanitizeContext(options, domPurify);
443
+ const ctx = new SanitizeContext(domPurify, opt);
439
444
  if (!ctx.schemeMap.get(scheme)) {
440
445
  return null;
441
446
  }
@@ -15,8 +15,9 @@ import {
15
15
  } from './utility.js';
16
16
 
17
17
  /* constants */
18
- import { DECI, MAX_BLOB_SIZE, MAX_URL_LENGTH } from './constant.js';
18
+ import { DECI, DEFAULT_OPTS, MAX_BLOB_SIZE } from './constant.js';
19
19
  import { REG_SCHEME, REG_SCRIPT, REG_SCRIPT_OR_BLOB } from './regexp.js';
20
+ /* @type {string[]} */
20
21
  const URL_PROPS = Object.freeze([
21
22
  'href',
22
23
  'origin',
@@ -30,46 +31,17 @@ const URL_PROPS = Object.freeze([
30
31
  'search',
31
32
  'hash'
32
33
  ]);
33
- const DEFAULT_OPTS = Object.freeze({
34
- allow: Object.freeze([]),
35
- deny: Object.freeze([]),
36
- only: Object.freeze([]),
37
- allowRelative: false,
38
- debug: false,
39
- revokeObjectURL: false,
40
- maxBlobSize: MAX_BLOB_SIZE,
41
- maxLength: MAX_URL_LENGTH
42
- });
43
34
 
44
35
  /* typedef */
45
36
  /**
46
- * The parsed result of a Data URL.
47
- * @typedef {object} InspectedDataURL
48
- * @property {boolean} base64 - Indicates whether the data is base64-encoded.
49
- * @property {string} data - The actual data part of the Data URL.
50
- * @property {string} mime - The MIME type of the data.
37
+ * @typedef {import('../index.js').InspectedDataURL} InspectedDataURL
38
+ * @typedef {import('../index.js').InspectedURLResult} InspectedURLResult
39
+ * @typedef {import('../index.js').SanitizeOptions} SanitizeOptions
51
40
  */
52
41
 
53
42
  /**
54
- * The result of an inspected URL, extending the standard URL API.
55
- * The properties except for input and valid are omitted for invalid URLs.
56
- * @typedef {object} InspectedURLResult
57
- * @property {string} input - The original URL input.
58
- * @property {boolean} valid - Indicates whether the sanitized URL (href) is a valid absolute URL.
59
- * @property {string|null} href - The sanitized URL, or null.
60
- * @property {boolean} [relative] - Indicates whether the sanitized URL (href) is a valid relative URL.
61
- * @property {string} [reason] - The reason why the URL is invalid.
62
- * @property {InspectedDataURL|null} [data] - The parsed result of a Data URL, or null.
63
- * @property {string} [origin] - The scheme, domain, and port.
64
- * @property {string} [protocol] - The protocol scheme.
65
- * @property {string} [username] - The specified username.
66
- * @property {string} [password] - The specified password.
67
- * @property {string} [host] - The domain and port.
68
- * @property {string} [hostname] - The domain.
69
- * @property {string} [port] - The port number.
70
- * @property {string} [pathname] - The path.
71
- * @property {string} [search] - The query string.
72
- * @property {string} [hash] - The fragment identifier.
43
+ * Internal sanitization options.
44
+ * @typedef {SanitizeOptions & { schemes?: Set<string> }} InternalSanitizeOptions
73
45
  */
74
46
 
75
47
  /* blob handlers */
@@ -114,6 +86,7 @@ const readStreamInChunksAsArrayBuffer = async (response, maxSize) => {
114
86
 
115
87
  /**
116
88
  * Fetches a Blob URL and extracts its ArrayBuffer and MIME type.
89
+ * @private
117
90
  * @param {string} url - The Blob URL to fetch.
118
91
  * @param {number} [maxBlobSize] - The maximum allowed Blob size in bytes.
119
92
  * @returns {Promise<{ buffer: ArrayBuffer, mimeType: string }>} A promise resolving to the buffer and MIME type.
@@ -210,11 +183,11 @@ export class URLSanitizer extends URISchemes {
210
183
  * Normalizes options and evaluates preliminary scheme routing.
211
184
  * @private
212
185
  * @param {string} url - The URL string.
213
- * @param {object} opt - User options.
214
- * @returns {{ isValid: boolean|undefined, options: object, scheme: string|null }} Nomalized options, scheme, and isValid.
186
+ * @param {SanitizeOptions} [opt] - The sanitization options.
187
+ * @returns {{ isValid: boolean|undefined, options: InternalSanitizeOptions, scheme: string|null }} Normalized options, scheme, and isValid.
215
188
  */
216
189
  #normalizeOptions(url, opt = {}) {
217
- if (!url || !isString(url)) {
190
+ if (!isString(url) || url === '') {
218
191
  return { isValid: false };
219
192
  }
220
193
  const options = {
@@ -258,7 +231,8 @@ export class URLSanitizer extends URISchemes {
258
231
  sanitizedURL = this.#filter.sanitize(url, {
259
232
  ...this.#defaultOpts,
260
233
  allow: ['data'],
261
- allowRelative: true
234
+ allowRelative: true,
235
+ schemes: this.#allowedSchemes
262
236
  });
263
237
  if (!sanitizedURL) {
264
238
  invalidReason =
@@ -366,23 +340,18 @@ export class URLSanitizer extends URISchemes {
366
340
  * @returns {void}
367
341
  */
368
342
  reset() {
369
- this.#allowedSchemes = new Set(super.get());
343
+ this.#allowedSchemes.clear();
344
+ for (const scheme of super.get()) {
345
+ this.#allowedSchemes.add(scheme);
346
+ }
370
347
  }
371
348
 
372
349
  /**
373
350
  * Asynchronously sanitizes the given URL.
374
351
  * NOTE: `blob`, `data`, and `file` schemes must be explicitly allowed.
375
352
  * Given a `blob` URL, it securely converts and returns a sanitized `data` URL.
376
- * @param {string} url - URL.
377
- * @param {object} [opt] - options.
378
- * @param {string[]} [opt.allow] - The array of schemes to allow.
379
- * @param {string[]} [opt.deny] - The array of schemes to deny.
380
- * @param {string[]} [opt.only] - The array of specific schemes to allow.
381
- * @param {boolean} [opt.allowRelative] - Allow relative URLs.
382
- * @param {boolean} [opt.debug] - Enable debug mode.
383
- * @param {boolean} [opt.revokeObjectURL] - Revokes the Blob URL after sanitization.
384
- * @param {number} [opt.maxBlobSize] - The maximum allowed Blob size in bytes.
385
- * @param {number} [opt.maxLength] - The maximum allowed URL length.
353
+ * @param {string} url - URL
354
+ * @param {SanitizeOptions} [opt] - The sanitization options.
386
355
  * @returns {Promise<string|null>} A promise resolving to the sanitized URL, or null.
387
356
  */
388
357
  async sanitizeURL(url, opt) {
@@ -393,47 +362,50 @@ export class URLSanitizer extends URISchemes {
393
362
  if (scheme === 'blob') {
394
363
  const { allow, deny, only } = options;
395
364
  let sanitizedData = null;
396
- if (
397
- (allow.includes('blob') && !deny.includes('blob')) ||
398
- only.includes('blob')
399
- ) {
400
- let fetchedBuffer = null;
401
- let fetchedMimeType = '';
402
- try {
403
- const fetchResult = await fetchBlobAsArrayBuffer(
404
- url,
405
- options.maxBlobSize
406
- );
407
- fetchedBuffer = fetchResult.buffer;
408
- fetchedMimeType = fetchResult.mimeType;
409
- } catch (e) {
410
- if (options.debug) {
411
- logDebug(
412
- `Failed to fetch and convert blob URL: ${truncateURL(url)}`,
413
- e
365
+ try {
366
+ if (
367
+ (allow.includes('blob') && !deny.includes('blob')) ||
368
+ only.includes('blob')
369
+ ) {
370
+ let fetchedBuffer = null;
371
+ let fetchedMimeType = '';
372
+ try {
373
+ const fetchResult = await fetchBlobAsArrayBuffer(
374
+ url,
375
+ options.maxBlobSize
414
376
  );
415
- }
416
- }
417
- if (fetchedBuffer) {
418
- if (only.length) {
419
- if (!only.includes('data')) {
420
- options.only = [...only, 'data'];
377
+ fetchedBuffer = fetchResult.buffer;
378
+ fetchedMimeType = fetchResult.mimeType;
379
+ } catch (e) {
380
+ if (options.debug) {
381
+ logDebug(
382
+ `Failed to fetch and convert blob URL: ${truncateURL(url)}`,
383
+ e
384
+ );
421
385
  }
422
- } else {
423
- if (!allow.includes('data')) {
424
- options.allow = [...allow, 'data'];
386
+ }
387
+ if (fetchedBuffer) {
388
+ if (only.length) {
389
+ if (!only.includes('data')) {
390
+ options.only = [...only, 'data'];
391
+ }
392
+ } else {
393
+ if (!allow.includes('data')) {
394
+ options.allow = [...allow, 'data'];
395
+ }
396
+ options.deny = deny.filter(s => s !== 'data');
425
397
  }
426
- options.deny = deny.filter(s => s !== 'data');
398
+ sanitizedData = await this.#filter.sanitizeBuffer(
399
+ fetchedBuffer,
400
+ fetchedMimeType,
401
+ options
402
+ );
427
403
  }
428
- sanitizedData = await this.#filter.sanitizeBuffer(
429
- fetchedBuffer,
430
- fetchedMimeType,
431
- options
432
- );
433
404
  }
434
- }
435
- if (options.revokeObjectURL) {
436
- URL.revokeObjectURL(url);
405
+ } finally {
406
+ if (options.revokeObjectURL) {
407
+ URL.revokeObjectURL(url);
408
+ }
437
409
  }
438
410
  return sanitizedData;
439
411
  } else if (scheme === 'data') {
@@ -447,14 +419,7 @@ export class URLSanitizer extends URISchemes {
447
419
  * NOTE: `data` and `file` schemes must be explicitly allowed.
448
420
  * The `blob` scheme is not supported and will return `null`.
449
421
  * @param {string} url - URL.
450
- * @param {object} [opt] - options.
451
- * @param {string[]} [opt.allow] - The array of schemes to allow.
452
- * @param {string[]} [opt.deny] - The array of schemes to deny.
453
- * @param {string[]} [opt.only] - The array of specific schemes to allow.
454
- * @param {boolean} [opt.allowRelative] - Allow relative URLs.
455
- * @param {boolean} [opt.debug] - Enable debug mode.
456
- * @param {boolean} [opt.revokeObjectURL] - Revokes the Blob URL.
457
- * @param {number} [opt.maxLength] - The maximum allowed URL length.
422
+ * @param {SanitizeOptions} [opt] - The sanitization options.
458
423
  * @returns {string|null} The sanitized URL, or null if denied.
459
424
  */
460
425
  sanitizeURLSync(url, opt) {
package/src/mjs/scheme.js CHANGED
@@ -123,10 +123,10 @@ export class URISchemes {
123
123
  * @returns {boolean} True if the parsed URL is valid and permitted.
124
124
  */
125
125
  verifyScheme(scheme, schemes, allowCustom = false) {
126
- if (!isString(scheme)) {
126
+ const normalized = this.normalize(scheme, true);
127
+ if (!normalized) {
127
128
  return false;
128
129
  }
129
- const normalized = this.normalize(scheme, true);
130
130
  const targetSchemes = schemes || this.#schemes;
131
131
  if (!normalized.includes('+')) {
132
132
  if (REG_SCRIPT.test(normalized)) {
@@ -49,7 +49,7 @@ export const WINDOWS1252_TO_UNICODE = new Map([
49
49
  /**
50
50
  * Generates hexadecimal escape strings for non-text codes.
51
51
  * @private
52
- * @returns {Map<number, string>} The map of non-text codes and hexadicimal escape strings.
52
+ * @returns {Map<number, string>} The map of non-text codes and hexadecimal escape strings.
53
53
  */
54
54
  const generateNonTextCharCodes = () => {
55
55
  const charCodes = new Map();