url-sanitizer 5.2.1 → 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/README.md +10 -12
- package/dist/url-sanitizer-wo-dompurify.min.js +1 -1
- package/dist/url-sanitizer-wo-dompurify.min.js.map +3 -3
- package/dist/url-sanitizer.min.js +3 -3
- package/dist/url-sanitizer.min.js.map +3 -3
- package/package.json +8 -8
- package/src/index.js +43 -18
- package/src/mjs/constant.js +16 -0
- package/src/mjs/dompurify.js +2 -2
- package/src/mjs/filter.js +48 -43
- package/src/mjs/sanitize.js +55 -94
- package/types/index.d.ts +29 -14
- package/types/mjs/constant.d.ts +1 -0
- package/types/mjs/filter.d.ts +11 -7
- package/types/mjs/sanitize.d.ts +7 -41
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.
|
|
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.
|
|
42
|
+
"eslint-plugin-jsdoc": "^64.3.5",
|
|
43
43
|
"eslint-plugin-prettier": "^5.5.6",
|
|
44
|
-
"eslint-plugin-regexp": "^3.
|
|
45
|
-
"eslint-plugin-unicorn": "^
|
|
44
|
+
"eslint-plugin-regexp": "^3.3.0",
|
|
45
|
+
"eslint-plugin-unicorn": "^74.0.0",
|
|
46
46
|
"fast-check": "^4.9.0",
|
|
47
|
-
"globals": "^17.
|
|
47
|
+
"globals": "^17.12.0",
|
|
48
48
|
"http-server": "^14.1.1",
|
|
49
49
|
"mitata": "^1.0.34",
|
|
50
|
-
"mocha": "^
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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 {
|
|
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 {
|
|
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) =>
|
package/src/mjs/constant.js
CHANGED
|
@@ -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
|
+
};
|
package/src/mjs/dompurify.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* dompurify.js
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
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 =
|
|
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 {
|
|
39
|
-
* @param {
|
|
47
|
+
* @param {DOMPurify} domPurifyInstance - The DOMPurify instance.
|
|
48
|
+
* @param {InternalSanitizeOptions} opt - The internal sanitization options.
|
|
40
49
|
*/
|
|
41
|
-
constructor(
|
|
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 {
|
|
68
|
+
* @param {InternalSanitizeOptions} opt - The internal sanitization options.
|
|
60
69
|
*/
|
|
61
|
-
#compileRules(opt
|
|
62
|
-
const { allowRelative, debug,
|
|
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 {
|
|
158
|
+
* @param {InternalSanitizeOptions} opt - The default sanitization options.
|
|
153
159
|
*/
|
|
154
|
-
constructor(
|
|
155
|
-
this.#defaultOpts =
|
|
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 =
|
|
269
|
+
let decodedDom = '';
|
|
264
270
|
try {
|
|
265
271
|
decodedDom = decodeURIComponent(dom);
|
|
266
272
|
} catch {
|
|
267
|
-
|
|
273
|
+
decodedDom = dom;
|
|
268
274
|
}
|
|
269
|
-
|
|
275
|
+
const hook = (node, evt) => this.#handleSanitizeAttribute(node, evt, ctx);
|
|
276
|
+
let purifiedDom = '';
|
|
270
277
|
try {
|
|
271
|
-
ctx.domPurify.addHook('uponSanitizeAttribute',
|
|
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
|
|
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 {
|
|
356
|
+
* @param {InternalSanitizeOptions} opt - The internal sanitization options.
|
|
350
357
|
* @returns {string|null} The sanitized URL, or null.
|
|
351
358
|
*/
|
|
352
|
-
sanitize(url,
|
|
359
|
+
sanitize(url, opt = this.#defaultOpts) {
|
|
353
360
|
if (!url || !isString(url)) {
|
|
354
361
|
return null;
|
|
355
362
|
}
|
|
356
|
-
const { deny, maxLength, only, allowRelative } =
|
|
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
|
|
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 &&
|
|
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(
|
|
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 {
|
|
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,
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
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 ${
|
|
405
|
+
`URL length ${dataUrl.length} exceeds max length ${opt.maxLength}.`
|
|
401
406
|
);
|
|
402
407
|
}
|
|
403
|
-
const ctx = new SanitizeContext(
|
|
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 {
|
|
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,
|
|
425
|
+
async sanitizeDataURL(url, opt = this.#defaultOpts) {
|
|
421
426
|
if (!url || !isString(url)) {
|
|
422
427
|
return null;
|
|
423
428
|
}
|
|
424
|
-
const { maxLength } =
|
|
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(
|
|
443
|
+
const ctx = new SanitizeContext(domPurify, opt);
|
|
439
444
|
if (!ctx.schemeMap.get(scheme)) {
|
|
440
445
|
return null;
|
|
441
446
|
}
|
package/src/mjs/sanitize.js
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
} from './utility.js';
|
|
16
16
|
|
|
17
17
|
/* constants */
|
|
18
|
-
import { DECI,
|
|
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
|
-
*
|
|
47
|
-
* @typedef {
|
|
48
|
-
* @
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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,10 +183,10 @@ 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 {
|
|
214
|
-
* @returns {{ isValid: boolean|undefined, options:
|
|
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
|
-
#normalizeOptions(url, opt) {
|
|
189
|
+
#normalizeOptions(url, opt = {}) {
|
|
217
190
|
if (!isString(url) || url === '') {
|
|
218
191
|
return { isValid: false };
|
|
219
192
|
}
|
|
@@ -377,67 +350,62 @@ export class URLSanitizer extends URISchemes {
|
|
|
377
350
|
* Asynchronously sanitizes the given URL.
|
|
378
351
|
* NOTE: `blob`, `data`, and `file` schemes must be explicitly allowed.
|
|
379
352
|
* Given a `blob` URL, it securely converts and returns a sanitized `data` URL.
|
|
380
|
-
* @param {string} url - URL
|
|
381
|
-
* @param {
|
|
382
|
-
* @param {string[]} [opt.allow] - The array of schemes to allow.
|
|
383
|
-
* @param {string[]} [opt.deny] - The array of schemes to deny.
|
|
384
|
-
* @param {string[]} [opt.only] - The array of specific schemes to allow.
|
|
385
|
-
* @param {boolean} [opt.allowRelative] - Allow relative URLs.
|
|
386
|
-
* @param {boolean} [opt.debug] - Enable debug mode.
|
|
387
|
-
* @param {boolean} [opt.revokeObjectURL] - Revokes the Blob URL after sanitization.
|
|
388
|
-
* @param {number} [opt.maxBlobSize] - The maximum allowed Blob size in bytes.
|
|
389
|
-
* @param {number} [opt.maxLength] - The maximum allowed URL length.
|
|
353
|
+
* @param {string} url - URL
|
|
354
|
+
* @param {SanitizeOptions} [opt] - The sanitization options.
|
|
390
355
|
* @returns {Promise<string|null>} A promise resolving to the sanitized URL, or null.
|
|
391
356
|
*/
|
|
392
357
|
async sanitizeURL(url, opt) {
|
|
393
|
-
const { isValid, options, scheme } = this.#normalizeOptions(url, opt
|
|
358
|
+
const { isValid, options, scheme } = this.#normalizeOptions(url, opt);
|
|
394
359
|
if (isValid === false) {
|
|
395
360
|
return null;
|
|
396
361
|
}
|
|
397
362
|
if (scheme === 'blob') {
|
|
398
363
|
const { allow, deny, only } = options;
|
|
399
364
|
let sanitizedData = null;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
fetchedBuffer = fetchResult.buffer;
|
|
412
|
-
fetchedMimeType = fetchResult.mimeType;
|
|
413
|
-
} catch (e) {
|
|
414
|
-
if (options.debug) {
|
|
415
|
-
logDebug(
|
|
416
|
-
`Failed to fetch and convert blob URL: ${truncateURL(url)}`,
|
|
417
|
-
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
|
|
418
376
|
);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
);
|
|
425
385
|
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
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');
|
|
429
397
|
}
|
|
430
|
-
|
|
398
|
+
sanitizedData = await this.#filter.sanitizeBuffer(
|
|
399
|
+
fetchedBuffer,
|
|
400
|
+
fetchedMimeType,
|
|
401
|
+
options
|
|
402
|
+
);
|
|
431
403
|
}
|
|
432
|
-
sanitizedData = await this.#filter.sanitizeBuffer(
|
|
433
|
-
fetchedBuffer,
|
|
434
|
-
fetchedMimeType,
|
|
435
|
-
options
|
|
436
|
-
);
|
|
437
404
|
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
405
|
+
} finally {
|
|
406
|
+
if (options.revokeObjectURL) {
|
|
407
|
+
URL.revokeObjectURL(url);
|
|
408
|
+
}
|
|
441
409
|
}
|
|
442
410
|
return sanitizedData;
|
|
443
411
|
} else if (scheme === 'data') {
|
|
@@ -451,18 +419,11 @@ export class URLSanitizer extends URISchemes {
|
|
|
451
419
|
* NOTE: `data` and `file` schemes must be explicitly allowed.
|
|
452
420
|
* The `blob` scheme is not supported and will return `null`.
|
|
453
421
|
* @param {string} url - URL.
|
|
454
|
-
* @param {
|
|
455
|
-
* @param {string[]} [opt.allow] - The array of schemes to allow.
|
|
456
|
-
* @param {string[]} [opt.deny] - The array of schemes to deny.
|
|
457
|
-
* @param {string[]} [opt.only] - The array of specific schemes to allow.
|
|
458
|
-
* @param {boolean} [opt.allowRelative] - Allow relative URLs.
|
|
459
|
-
* @param {boolean} [opt.debug] - Enable debug mode.
|
|
460
|
-
* @param {boolean} [opt.revokeObjectURL] - Revokes the Blob URL.
|
|
461
|
-
* @param {number} [opt.maxLength] - The maximum allowed URL length.
|
|
422
|
+
* @param {SanitizeOptions} [opt] - The sanitization options.
|
|
462
423
|
* @returns {string|null} The sanitized URL, or null if denied.
|
|
463
424
|
*/
|
|
464
425
|
sanitizeURLSync(url, opt) {
|
|
465
|
-
const { isValid, options, scheme } = this.#normalizeOptions(url, opt
|
|
426
|
+
const { isValid, options, scheme } = this.#normalizeOptions(url, opt);
|
|
466
427
|
if (isValid === false) {
|
|
467
428
|
return null;
|
|
468
429
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -6,10 +6,30 @@
|
|
|
6
6
|
* @see {@link https://github.com/asamuzaK/urlSanitizer/blob/main/LICENSE}
|
|
7
7
|
*/
|
|
8
8
|
import { URLSanitizer } from './mjs/sanitize.js';
|
|
9
|
-
export type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export type InspectedDataURL = {
|
|
10
|
+
base64: boolean;
|
|
11
|
+
data: string;
|
|
12
|
+
mime: string;
|
|
13
|
+
};
|
|
14
|
+
export type InspectedURLResult = {
|
|
15
|
+
input: string;
|
|
16
|
+
valid: boolean;
|
|
17
|
+
href: string | null;
|
|
18
|
+
relative?: boolean;
|
|
19
|
+
reason?: string;
|
|
20
|
+
data?: InspectedDataURL | null;
|
|
21
|
+
origin?: string;
|
|
22
|
+
protocol?: string;
|
|
23
|
+
username?: string;
|
|
24
|
+
password?: string;
|
|
25
|
+
host?: string;
|
|
26
|
+
hostname?: string;
|
|
27
|
+
port?: string;
|
|
28
|
+
pathname?: string;
|
|
29
|
+
search?: string;
|
|
30
|
+
hash?: string;
|
|
31
|
+
};
|
|
32
|
+
export type SanitizeOptions = {
|
|
13
33
|
allow?: string[];
|
|
14
34
|
deny?: string[];
|
|
15
35
|
only?: string[];
|
|
@@ -18,16 +38,11 @@ export declare const sanitizeURL: (url: string, opt?: {
|
|
|
18
38
|
revokeObjectURL?: boolean;
|
|
19
39
|
maxBlobSize?: number;
|
|
20
40
|
maxLength?: number;
|
|
21
|
-
}
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
allowRelative?: boolean;
|
|
27
|
-
debug?: boolean;
|
|
28
|
-
revokeObjectURL?: boolean;
|
|
29
|
-
maxLength?: number;
|
|
30
|
-
}) => string | null;
|
|
41
|
+
};
|
|
42
|
+
export { URLSanitizer };
|
|
43
|
+
declare const sanitizer: URLSanitizer;
|
|
44
|
+
export declare const sanitizeURL: (url: string, opt?: SanitizeOptions) => Promise<string | null>;
|
|
45
|
+
export declare const sanitizeURLSync: (url: string, opt?: SanitizeOptions) => string | null;
|
|
31
46
|
export declare const inspectURL: (url: string) => Promise<InspectedURLResult>;
|
|
32
47
|
export declare const isValidURI: (uri: string) => boolean;
|
|
33
48
|
export declare const isURISync: (uri: string) => boolean;
|
package/types/mjs/constant.d.ts
CHANGED