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/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 +59 -94
- package/src/mjs/scheme.js +2 -2
- package/src/mjs/text-chars.js +1 -1
- 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,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 {
|
|
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
189
|
#normalizeOptions(url, opt = {}) {
|
|
217
|
-
if (!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
|
|
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 {
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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 {
|
|
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
|
-
|
|
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)) {
|
package/src/mjs/text-chars.js
CHANGED
|
@@ -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
|
|
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();
|