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/README.md +24 -8
- 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 +6 -1
- package/src/mjs/sanitizer.js +58 -18
- package/types/mjs/sanitizer.d.ts +2 -0
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.
|
|
87
|
+
"version": "2.3.0"
|
|
83
88
|
}
|
package/src/mjs/sanitizer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
}
|
package/types/mjs/sanitizer.d.ts
CHANGED
|
@@ -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;
|