url-sanitizer 2.2.0 → 2.3.1
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 +26 -9
- 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 +68 -39
- 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.1"
|
|
83
88
|
}
|
package/src/mjs/sanitizer.js
CHANGED
|
@@ -84,9 +84,15 @@ class URLSanitizer extends URISchemes {
|
|
|
84
84
|
}
|
|
85
85
|
this.#nest++;
|
|
86
86
|
this.#recurse.add(dataUrl);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
let parsedDataUrl;
|
|
88
|
+
try {
|
|
89
|
+
parsedDataUrl = this.sanitize(dataUrl, {
|
|
90
|
+
allow: ['data']
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
this.#nest--;
|
|
94
|
+
this.#recurse.delete(dataUrl);
|
|
95
|
+
}
|
|
90
96
|
const { index } = item;
|
|
91
97
|
const [preDataUrl, postDataUrl] = [
|
|
92
98
|
replacedData.substring(0, index),
|
|
@@ -111,13 +117,26 @@ class URLSanitizer extends URISchemes {
|
|
|
111
117
|
if (!isString(dom)) {
|
|
112
118
|
throw new TypeError(`Expected String but got ${getType(dom)}.`);
|
|
113
119
|
}
|
|
114
|
-
let
|
|
120
|
+
let decodedDom = dom;
|
|
121
|
+
try {
|
|
122
|
+
// prevent URIError caused by malformed percent-encoding
|
|
123
|
+
// e.g., a standalone '%'
|
|
124
|
+
decodedDom = decodeURIComponent(dom);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// fall through
|
|
127
|
+
}
|
|
128
|
+
let purifiedDom = domPurify.sanitize(decodedDom);
|
|
115
129
|
if (purifiedDom && REG_DATA_URL.test(purifiedDom)) {
|
|
116
130
|
purifiedDom = this.replace(purifiedDom);
|
|
117
131
|
}
|
|
118
132
|
purifiedDom = purifiedDom.replace(/(?:#|%23)$/, '')
|
|
119
133
|
.replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/, '');
|
|
120
|
-
|
|
134
|
+
try {
|
|
135
|
+
// prevent URIError caused by lone surrogates
|
|
136
|
+
return encodeURI(purifiedDom);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return purifiedDom;
|
|
139
|
+
}
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
/**
|
|
@@ -130,19 +149,22 @@ class URLSanitizer extends URISchemes {
|
|
|
130
149
|
* @param {Array.<string>} [opt.allow] - array of allowed schemes
|
|
131
150
|
* @param {Array.<string>} [opt.deny] - array of denied schemes
|
|
132
151
|
* @param {Array.<string>} [opt.only] - array of specific schemes to allow
|
|
152
|
+
* @param {boolean} [opt.allowRelative] - allow relative URLs
|
|
153
|
+
* @param {boolean} [opt.debug] - enable debug mode
|
|
133
154
|
* @returns {?string} - sanitized URL
|
|
134
155
|
*/
|
|
135
156
|
sanitize(url, opt) {
|
|
136
157
|
if (this.#nest > HEX) {
|
|
137
|
-
this.#nest = 0;
|
|
138
158
|
throw new Error('Data URLs nested too deeply.');
|
|
139
159
|
}
|
|
140
|
-
const {
|
|
160
|
+
const {
|
|
161
|
+
allow, deny, only, allowRelative = false, debug = false
|
|
162
|
+
} = opt ?? {};
|
|
141
163
|
const schemeMap = new Map([
|
|
142
164
|
['blob', false],
|
|
143
165
|
['data', false],
|
|
144
166
|
['file', false],
|
|
145
|
-
['
|
|
167
|
+
['javascript', false],
|
|
146
168
|
['vbscript', false]
|
|
147
169
|
]);
|
|
148
170
|
const tempScheme = new Set();
|
|
@@ -215,25 +237,46 @@ class URLSanitizer extends URISchemes {
|
|
|
215
237
|
}
|
|
216
238
|
}
|
|
217
239
|
let sanitizedUrl;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
let isVerified = super.verify(url);
|
|
241
|
+
let isRelative = false;
|
|
242
|
+
if (!isVerified && allowRelative) {
|
|
243
|
+
try {
|
|
244
|
+
const { hostname, protocol } = new URL(url, 'http://dummy.local');
|
|
245
|
+
if (protocol === 'http:' && hostname === 'dummy.local') {
|
|
246
|
+
isVerified = true;
|
|
247
|
+
isRelative = true;
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
logDebug(debug, 'Failed to parse relative URL.', e);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (isVerified) {
|
|
254
|
+
let hash, href, pathname, protocol, search;
|
|
255
|
+
let scheme = '';
|
|
256
|
+
let schemeParts = [];
|
|
257
|
+
let bool = false;
|
|
258
|
+
if (isRelative) {
|
|
259
|
+
bool = true;
|
|
225
260
|
} else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
const urlObj = new URL(url);
|
|
262
|
+
({ hash, href, pathname, protocol, search } = urlObj);
|
|
263
|
+
scheme = protocol.replace(/:$/, '');
|
|
264
|
+
schemeParts = scheme.split('+');
|
|
265
|
+
if (restrictScheme) {
|
|
266
|
+
bool = schemeParts.every(s => schemeMap.get(s));
|
|
267
|
+
} else {
|
|
268
|
+
for (const [key, value] of schemeMap.entries()) {
|
|
269
|
+
bool =
|
|
270
|
+
value || (scheme !== key && schemeParts.every(s => s !== key));
|
|
271
|
+
if (!bool) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
230
274
|
}
|
|
231
275
|
}
|
|
232
276
|
}
|
|
233
277
|
if (bool) {
|
|
234
|
-
const isDataUrl = schemeParts.includes('data');
|
|
235
|
-
let
|
|
236
|
-
let urlToSanitize = href;
|
|
278
|
+
const isDataUrl = isRelative ? false : schemeParts.includes('data');
|
|
279
|
+
let urlToSanitize = isRelative ? url : href;
|
|
237
280
|
if (isDataUrl) {
|
|
238
281
|
const [mediaType, ...dataParts] = pathname.split(',');
|
|
239
282
|
const data = `${dataParts.join(',')}${search}${hash}`;
|
|
@@ -258,15 +301,7 @@ class URLSanitizer extends URISchemes {
|
|
|
258
301
|
if (parsedData !== data || containsDataUrl) {
|
|
259
302
|
if (containsDataUrl) {
|
|
260
303
|
parsedData = this.replace(parsedData);
|
|
261
|
-
} else if (this.#recurse.has(url)) {
|
|
262
|
-
this.#recurse.delete(url);
|
|
263
|
-
} else {
|
|
264
|
-
finalize = true;
|
|
265
304
|
}
|
|
266
|
-
} else if (this.#recurse.has(url)) {
|
|
267
|
-
this.#recurse.delete(url);
|
|
268
|
-
} else {
|
|
269
|
-
finalize = true;
|
|
270
305
|
}
|
|
271
306
|
if (!mediaType || REG_MIME_DOM.test(mediaType)) {
|
|
272
307
|
parsedData = this.purify(parsedData);
|
|
@@ -279,8 +314,6 @@ class URLSanitizer extends URISchemes {
|
|
|
279
314
|
} else {
|
|
280
315
|
urlToSanitize = '';
|
|
281
316
|
}
|
|
282
|
-
} else {
|
|
283
|
-
finalize = true;
|
|
284
317
|
}
|
|
285
318
|
if (!isDataUrl && REG_TAG_QUOT.test(urlToSanitize)) {
|
|
286
319
|
const item = REG_TAG_QUOT.exec(urlToSanitize);
|
|
@@ -291,11 +324,6 @@ class URLSanitizer extends URISchemes {
|
|
|
291
324
|
if (urlToSanitize) {
|
|
292
325
|
sanitizedUrl =
|
|
293
326
|
urlToSanitize.replace(/%26/g, escapeURLEncodedHTMLChars);
|
|
294
|
-
if (finalize) {
|
|
295
|
-
this.#nest = 0;
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
this.#nest = 0;
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
329
|
if (tempScheme.size) {
|
|
@@ -399,6 +427,7 @@ export const sanitizeURL = async (url, opt = {
|
|
|
399
427
|
allow: [],
|
|
400
428
|
deny: [],
|
|
401
429
|
only: [],
|
|
430
|
+
allowRelative: false,
|
|
402
431
|
debug: false,
|
|
403
432
|
maxBlobSize: MAX_BLOB_SIZE
|
|
404
433
|
}) => {
|
|
@@ -447,7 +476,7 @@ export const sanitizeURL = async (url, opt = {
|
|
|
447
476
|
}
|
|
448
477
|
}
|
|
449
478
|
URL.revokeObjectURL(url);
|
|
450
|
-
} else if (scheme) {
|
|
479
|
+
} else if (scheme || opt.allowRelative) {
|
|
451
480
|
res = urlSanitizer.sanitize(url, opt);
|
|
452
481
|
}
|
|
453
482
|
}
|
|
@@ -478,7 +507,7 @@ export const sanitizeURLSync = (url, opt) => {
|
|
|
478
507
|
}
|
|
479
508
|
if (scheme === 'blob') {
|
|
480
509
|
URL.revokeObjectURL(url);
|
|
481
|
-
} else if (scheme) {
|
|
510
|
+
} else if (scheme || opt?.allowRelative) {
|
|
482
511
|
res = urlSanitizer.sanitize(url, opt);
|
|
483
512
|
}
|
|
484
513
|
}
|
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;
|