url-sanitizer 4.2.2 → 5.0.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 +45 -48
- 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 +4 -4
- package/dist/url-sanitizer.min.js.map +3 -3
- package/package.json +16 -19
- package/src/index.js +2 -5
- package/src/lib/iana/uri-schemes.json +1 -1
- package/src/mjs/constant.js +18 -0
- package/src/mjs/regexp.js +17 -11
- package/src/mjs/sanitizer.js +476 -319
- package/src/mjs/text-chars.js +6 -6
- package/src/mjs/uri-util.js +195 -139
- package/types/index.d.ts +1 -1
- package/types/mjs/constant.d.ts +3 -0
- package/types/mjs/regexp.d.ts +3 -2
- package/types/mjs/sanitizer.d.ts +15 -8
- package/types/mjs/text-chars.d.ts +1 -1
- package/types/mjs/uri-util.d.ts +9 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/asamuzaK/urlSanitizer/releases)
|
|
7
7
|
|
|
8
8
|
A robust URL sanitizer for Node.js, browsers, and websites.
|
|
9
|
-
It sanitizes
|
|
9
|
+
It sanitizes regular URLs and performs deep inspection of data and blob URLs.
|
|
10
10
|
It also provides built-in utilities to inspect URLs and verify URI schemes.
|
|
11
11
|
|
|
12
12
|
## Table of Contents
|
|
@@ -17,9 +17,7 @@ It also provides built-in utilities to inspect URLs and verify URI schemes.
|
|
|
17
17
|
* [sanitizeURL(url, opt)](#sanitizeurlurl-opt)
|
|
18
18
|
* [sanitizeURLSync(url, opt)](#sanitizeurlsyncurl-opt)
|
|
19
19
|
* [inspectURL(url)](#inspecturlurl)
|
|
20
|
-
* [
|
|
21
|
-
* [isURI(uri)](#isuriuri)
|
|
22
|
-
* [isURISync(uri)](#isurisyncuri)
|
|
20
|
+
* [isValidURI(uri)](#isvaliduriuri)
|
|
23
21
|
* [urlSanitizer Instance](#urlsanitizer)
|
|
24
22
|
* [Threat Model](#threat-model)
|
|
25
23
|
* [AI / LLM Application Security](#ai--llm-application-security)
|
|
@@ -32,6 +30,7 @@ It also provides built-in utilities to inspect URLs and verify URI schemes.
|
|
|
32
30
|
* **Deep Data URL Inspection**: Parses, decodes (including base64), and sanitizes nested data URLs.
|
|
33
31
|
* **Blob URL Support**: Fetches the underlying blob content, purifies nested XSS vectors via DOMPurify, and outputs a safe data URL (async).
|
|
34
32
|
* **Relative & Absolute Path Support**: Safely allows root-relative paths (e.g., `/foo`) and relative paths (e.g., `./foo`) via an opt-in parameter.
|
|
33
|
+
* **URL Inspection API**: Returns parsed URL components after sanitization.
|
|
35
34
|
|
|
36
35
|
## Install
|
|
37
36
|
|
|
@@ -77,7 +76,7 @@ If you use this build, ensure DOMPurify is exposed globally (e.g., `window.DOMPu
|
|
|
77
76
|
|
|
78
77
|
``` javascript
|
|
79
78
|
import urlSanitizer, {
|
|
80
|
-
|
|
79
|
+
inspectURL, isValidURI, sanitizeURL, sanitizeURLSync
|
|
81
80
|
} from 'url-sanitizer';
|
|
82
81
|
```
|
|
83
82
|
|
|
@@ -111,7 +110,7 @@ Sanitizes the given URL asynchronously.
|
|
|
111
110
|
* opt.debug **\[boolean\]** If `true`, outputs internal error/warning logs to the console. Default is `false`.
|
|
112
111
|
* opt.revokeObjectURL **\[boolean\]** Revokes the blob URL after sanitization. Default is `false`. However, to prevent memory leaks, it is highly recommended to set this option to `true`.
|
|
113
112
|
* opt.maxBlobSize **\[number\]** Maximum allowed blob size in bytes. Default is `16_777_216` (16MB).
|
|
114
|
-
* opt.maxLength **\[number\]** Maximum allowed URL length. Default is
|
|
113
|
+
* opt.maxLength **\[number\]** Maximum allowed URL length. Default is `65_536` (64KB).
|
|
115
114
|
|
|
116
115
|
**Returns** **Promise<string?>** Sanitized URL, nullable.
|
|
117
116
|
|
|
@@ -162,7 +161,9 @@ console.log(decodeURIComponent(res4));
|
|
|
162
161
|
// => 'data:text/html,<div></div>'
|
|
163
162
|
|
|
164
163
|
const base64data4_2 = btoa('<div><img src="javascript:alert(1)"></div>');
|
|
165
|
-
const res4_2 = await sanitizeURL(`data:text/html;base64,${base64data4_2}
|
|
164
|
+
const res4_2 = await sanitizeURL(`data:text/html;base64,${base64data4_2}`, {
|
|
165
|
+
allow: ['data']
|
|
166
|
+
});
|
|
166
167
|
// => 'data:text/html,%3Cdiv%3E%3Cimg%3E%3C/div%3E'
|
|
167
168
|
|
|
168
169
|
console.log(decodeURIComponent(res4_2));
|
|
@@ -223,19 +224,16 @@ Synchronous version of `sanitizeURL()`.
|
|
|
223
224
|
|
|
224
225
|
### inspectURL(url)
|
|
225
226
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
Inspects, parses, and sanitizes the given URL asynchronously.
|
|
227
|
+
Sanitizes the given URL and returns its parsed components.
|
|
229
228
|
|
|
230
229
|
* **Data URLs:** The embedded payload is fully decoded and sanitized (e.g., removing malicious HTML/SVG attributes) before being safely re-encoded.
|
|
231
|
-
* **Blob URLs:**
|
|
232
|
-
To process and sanitize the content of a blob URL, use [sanitizeURL()](#sanitizeurlurl-opt).
|
|
230
|
+
* **Blob URLs:** Blob URLs are verified after being converted to data URLs. Note that blob URLs are not revoked.
|
|
233
231
|
|
|
234
232
|
#### Parameters
|
|
235
233
|
|
|
236
|
-
* url **string** URL
|
|
234
|
+
* url **string** The URL string to sanitize and inspect.
|
|
237
235
|
|
|
238
|
-
**Returns** **Promise<InspectedURLResult>**
|
|
236
|
+
**Returns** **Promise<InspectedURLResult>** The parsed components of the sanitized URL.
|
|
239
237
|
|
|
240
238
|
#### InspectedURLResult
|
|
241
239
|
|
|
@@ -307,7 +305,6 @@ const res4 = await inspectURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAU
|
|
|
307
305
|
...
|
|
308
306
|
} */
|
|
309
307
|
|
|
310
|
-
// Note that blob URLs are parsed but not yet sanitized
|
|
311
308
|
const blob5 = new Blob(['<svg><g onload="alert(1)"/></svg>'], {
|
|
312
309
|
type: 'image/svg+xml'
|
|
313
310
|
});
|
|
@@ -316,22 +313,20 @@ const res5 = await inspectURL(url5);
|
|
|
316
313
|
/* => {
|
|
317
314
|
input: 'blob:nodedata:82ecc5a4-aea8-48d7-a407-64e2ef0913da',
|
|
318
315
|
valid: true,
|
|
319
|
-
data:
|
|
320
|
-
|
|
316
|
+
data: {
|
|
317
|
+
mime: 'image/svg+xml',
|
|
318
|
+
base64: false,
|
|
319
|
+
data: '%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E'
|
|
320
|
+
},
|
|
321
|
+
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
321
322
|
origin: 'null',
|
|
322
|
-
protocol: '
|
|
323
|
-
pathname: '
|
|
323
|
+
protocol: 'data:',
|
|
324
|
+
pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
324
325
|
...
|
|
325
326
|
} */
|
|
326
327
|
```
|
|
327
328
|
|
|
328
|
-
###
|
|
329
|
-
|
|
330
|
-
**Note:** `parseURLSync()` is deprecated. Please use `inspectURLSync()` instead.
|
|
331
|
-
|
|
332
|
-
Synchronous version of `inspectURL()`.
|
|
333
|
-
|
|
334
|
-
### isURI(uri)
|
|
329
|
+
### isValidURI(uri)
|
|
335
330
|
|
|
336
331
|
Checks if the given string is a valid URI and whether its scheme is registered in the allowed list.
|
|
337
332
|
|
|
@@ -339,39 +334,37 @@ Specifically, it verifies that the input has a correct URI syntax, is not a deni
|
|
|
339
334
|
* The scheme is registered in the default list (e.g., IANA registered schemes, `https`, `mailto`).
|
|
340
335
|
* The scheme has a custom prefix like `web+` or `ext+` (which are always allowed for web applications).
|
|
341
336
|
|
|
337
|
+
**Note:** `isURISync()` is deprecated. Please use `isValidURI()` instead.
|
|
338
|
+
|
|
342
339
|
#### Parameters
|
|
343
340
|
|
|
344
341
|
* uri **string** URI input.
|
|
345
342
|
|
|
346
|
-
**Returns** **
|
|
343
|
+
**Returns** **boolean** `true` if the URI is syntactically valid and uses an allowed/registered scheme; otherwise `false`.
|
|
347
344
|
|
|
348
345
|
* Always `true` for valid `web+*` and `ext+*` schemes (except `web+javascript`, `web+vbscript`, `ext+javascript`, and `ext+vbscript`).
|
|
349
346
|
* Always `false` for `javascript` and `vbscript` schemes, or any unknown/unregistered schemes (e.g., `foo:`).
|
|
350
347
|
|
|
351
348
|
``` javascript
|
|
352
|
-
const res1 =
|
|
349
|
+
const res1 = isValidURI('https://example.com/foo');
|
|
353
350
|
// => true
|
|
354
351
|
|
|
355
|
-
const res2 =
|
|
352
|
+
const res2 = isValidURI('javascript:alert(1)');
|
|
356
353
|
// => false
|
|
357
354
|
|
|
358
|
-
const res3 =
|
|
355
|
+
const res3 = isValidURI('mailto:foo@example.com');
|
|
359
356
|
// => true
|
|
360
357
|
|
|
361
|
-
const res4 =
|
|
358
|
+
const res4 = isValidURI('foo:bar');
|
|
362
359
|
// => false
|
|
363
360
|
|
|
364
|
-
const res5 =
|
|
361
|
+
const res5 = isValidURI('web+foo:bar');
|
|
365
362
|
// => true
|
|
366
363
|
|
|
367
|
-
const res6 =
|
|
364
|
+
const res6 = isValidURI('web+javascript:alert(1)');
|
|
368
365
|
// => false
|
|
369
366
|
```
|
|
370
367
|
|
|
371
|
-
### isURISync(uri)
|
|
372
|
-
|
|
373
|
-
Synchronous version of `isURI()`.
|
|
374
|
-
|
|
375
368
|
### urlSanitizer
|
|
376
369
|
|
|
377
370
|
The instance of the sanitizer.
|
|
@@ -431,7 +424,7 @@ Because `url-sanitizer` performs **Deep Data URL Inspection** — decoding the p
|
|
|
431
424
|
|
|
432
425
|
### Neutralizing Hallucinated Schemes
|
|
433
426
|
LLMs generate URLs based on statistical linguistic patterns rather than factual databases.
|
|
434
|
-
As a result, they
|
|
427
|
+
As a result, they may generate hallucinate plausible-looking but non-existent or hazardous URI schemes (e.g., `ai-agent://`, `host-settings:`).
|
|
435
428
|
By operating on a whitelist approach, `url-sanitizer` automatically denies any unrecognized or unregistered protocols.
|
|
436
429
|
This strict blocking prevents non-existent or hallucinated schemes from inadvertently triggering application-specific or OS-level protocol hijacking.
|
|
437
430
|
|
|
@@ -443,22 +436,26 @@ Execution times were measured using [mitata](https://github.com/evanwashere/mita
|
|
|
443
436
|
|
|
444
437
|
| URL Type | `url-sanitizer` | [@braintree/sanitize-url](https://www.npmjs.com/package/@braintree/sanitize-url) | [strict-url-sanitise](https://www.npmjs.com/package/strict-url-sanitise) |
|
|
445
438
|
| :--- | :---: | :---: | :---: |
|
|
446
|
-
| **Normal HTTP URL** | ~
|
|
447
|
-
| **XSS URL** | ~5.
|
|
448
|
-
| **
|
|
449
|
-
| **
|
|
439
|
+
| **Normal HTTP URL** | ~1.25 µs/iter | ~4.33 µs/iter | ~4.68 µs/iter |
|
|
440
|
+
| **XSS URL** | ~5.68 µs/iter | ~1.63 µs/iter | ~9.44 µs/iter |
|
|
441
|
+
| **Data URL** | ~371.03 µs/iter | ~2.85 µs/iter | ~9.44 µs/iter |
|
|
442
|
+
| **Blob URL** | ~552.58 µs/iter | ~2.18 µs/iter | ~9.11 µs/iter |
|
|
443
|
+
| **Invalid URL** | ~0.28 µs/iter | ~1.35 µs/iter | ~21.22 µs/iter |
|
|
450
444
|
|
|
451
445
|
### Characteristics & Trade-offs
|
|
452
446
|
|
|
453
447
|
* **Optimized for Standard Routing & XSS Rejection**
|
|
454
|
-
* For **
|
|
448
|
+
* For **normal HTTP(S) URLs** and URLs containing common XSS patterns, `url-sanitizer` achieves low latency by leveraging the native `URL` API and applying optimized early-return paths when strict inspection is unnecessary.
|
|
455
449
|
* **Exceptional Invalid URL Handling (Fail-Fast)**
|
|
456
|
-
* For **
|
|
450
|
+
* For **invalid URLs** (such as malformed schemes or unparseable formats), `url-sanitizer` uses a fail-fast architecture based on `URL.parse()`, avoiding unnecessary sanitization steps after validation failure.
|
|
457
451
|
* **Deep Inspection for High-Risk Payloads**
|
|
458
|
-
* For **
|
|
459
|
-
1.
|
|
460
|
-
2.
|
|
461
|
-
3.
|
|
452
|
+
* For **Data URLs** and **Blob URLs**, `url-sanitizer` prioritizes security over raw execution speed. The following inspection pipeline is applied:
|
|
453
|
+
1. If Blob URL is given, convert it to a Data URL.
|
|
454
|
+
2. Parses and normalizes the Data URL structure.
|
|
455
|
+
3. Decodes Base64 payloads when present.
|
|
456
|
+
4. Decodes URL-encoded content and numeric character references.
|
|
457
|
+
5. Sanitizes embedded HTML/SVG content using `DOMPurify`.
|
|
458
|
+
6. Re-encodes the purified payload into a safe URL representation.
|
|
462
459
|
|
|
463
460
|
## Acknowledgments
|
|
464
461
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var re=Object.freeze(["addHook","removeHook","removeAllHooks","setConfig"]),U=[],S=new Proxy({},{get(s,e){let t=globalThis.DOMPurify||globalThis.window?.DOMPurify;if(t){if(U.length){for(let{prop:a,args:n}of U)typeof t[a]=="function"&&t[a](...n);U.length=0}let r=t[e];return typeof r=="function"?r.bind(t):r}return(...r)=>{if(re.includes(e))return U.push({prop:e,args:r}),s;throw new Error("DOMPurify is not available. Ensure DOMPurify is exposed globally (e.g., window.DOMPurify).")}}});var R=s=>Object.prototype.toString.call(s).slice(8,-1),p=s=>typeof s=="string"||s instanceof String;var k=["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","agtp","aim","amss","android","appdata","apt","ar","ari","ark","at","attachment","aw","barion","beshare","bitcoin","bitcoincash","bl","blob","bluetooth","bolo","brid","browserext","cabal","caip","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cttps","cvs","dab","dat","data","dav","dhttp","diaspora","dict","did","dilithium3","dis","dlna-playcontainer","dlna-playsingle","dnp","dns","dntp","doi","donau","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","esim","ethereum","example","ez","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","mdoc","mdoc-openid4vp","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","musik","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","nostr","notes","npamp","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","sss","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","tttps","turn","turns","tv","udp","unreal","upn","urn","ust","ut2004","uuaid","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"];var D=/(?:#|%23)$/,G=/^(?:application\/(?:[\w.-]+\+)?xml|image\/svg\+xml|text\/(?:html|xml))(?:\s*;\s*|$)/i;var B=/&#(x0*[\dA-F]{1,2}|0*\d{1,3});?/gi,j=/^((?:[^#%]|%(?!23))*)(?:\?|%3F)$/i,A=/^[a-z][\da-z+.-]*$/,F=/^(?:web\+[a-z]+|ext\+[a-z][\da-z.-]*)$/,w=/^(?:java|vb)script$/i,z=/^(?:(?:java|vb)script|blob)$/i,v=/["'<>]|%(?:22|27|3C|3E)|&(?:apos|lt|gt|quot|#0*(?:34|39|60|62)|#x0*(?:22|27|3C|3E));/i,H=/^%[\dA-F]{2}$/i,I=/&(?=[#a-z0-9])(?![a-z0-9]+=)/i,X=/(?:%[\dA-F]{2})+/gi,V=/^(?:\/{2,}|\\|[a-z][a-z\d+\-.]*:[^/])/i;var Y=[9,10,13,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255];var P=new Set(Y),L=new Map([[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),ce=()=>{let s=new Map;for(let e=0;e<256;e++)!P.has(e)&&!L.has(e)&&s.set(e,`\\x${e.toString(16).padStart(2,"0").toUpperCase()}`);return s},_=ce();var[b,pe,me,de,fe,ue]=["&","#","<",">",'"',"'"].map(s=>`%${s.charCodeAt(0).toString(16).toUpperCase()}`),he={[b]:`${b}amp;`,[me]:`${b}lt;`,[de]:`${b}gt;`,[fe]:`${b}quot;`,[ue]:`${b}${pe}39;`},Ye=Array.from({length:256},(s,e)=>`%${e.toString(16).padStart(2,"0").toUpperCase()}`),Q=globalThis.process?.versions?.node!==void 0,ge=new RegExp(`[${[..._.values()].join("")}]`),ye=new RegExp(`[${[..._.values()].join("")}]`,"g"),qe=new TextEncoder,Re=new TextDecoder("utf-8",{fatal:!0}),T=class{#e=new Set(k);get(){return[...this.#e]}has(e){return p(e)?this.#e.has(e.trim().toLowerCase()):!1}verify(e,t=this.#e){if(!p(e))return!1;let r=e.normalize("NFKC"),a=URL.parse(r);if(!a)return!1;let n=a.protocol.replace(/:$/,""),o=n.split("+");return o.some(i=>w.test(i))?!1:F.test(n)||o.every(i=>t.has(i))}};var K=s=>{if(!p(s))return s;let e=H.test(s)?s.toUpperCase():s;return he[e]??s},W=s=>{if(!p(s))throw new TypeError(`Expected String but got ${R(s)}.`);let e=s.replace(/\s/g,""),t;try{t=atob(e)}catch{throw new Error(`Invalid base64 data: ${s}`)}let r;if(Q&&globalThis.Buffer)r=globalThis.Buffer.from(t,"latin1");else{let a=t.length;r=new Uint8Array(a);for(let n=0;n<a;n++)r[n]=t.charCodeAt(n)}try{let a=Re.decode(r);return ge.test(a)?e:a}catch{return e}},xe=(s,e)=>{let t=/x/i.test(e[0])?parseInt(e.substring(1),16):parseInt(e,10);if(Number.isInteger(t)){if(P.has(t))return String.fromCharCode(t);if(L.has(t)){let r=L.get(t);return String.fromCodePoint(r)}else if(_.has(t))return""}return s},Z=s=>p(s)?s.replace(D,"").replace(j,"$1"):s,J=(s,e=0)=>{if(!p(s))throw new TypeError(`Expected String but got ${R(s)}.`);if(!Number.isInteger(e))throw new TypeError(`Expected Number but got ${R(e)}.`);let t;try{t=decodeURIComponent(s)}catch{t=s.replace(X,a=>{try{return decodeURIComponent(a)}catch{return a}})}t=t.replace(ye,"");let r=0;for(;r+e<=16;r++){let a=t;if(t=t.replace(/&/gi,"&").replace(B,xe),t===a)break}if(r+e>16&&/&#/.test(t))throw new Error("Character references nested too deeply.");return t},be=async s=>{let e=s.type?`${s.type};base64`:"base64",t=await s.arrayBuffer(),r=globalThis.Buffer.from(t).toString("base64");return`data:${e},${r}`},Ee=s=>new Promise((e,t)=>{let r=new globalThis.FileReader;r.addEventListener("error",()=>t(r.error)),r.addEventListener("abort",()=>e(null)),r.addEventListener("load",()=>e(r.result)),r.readAsDataURL(s)}),we=async s=>{let e=s.type?`${s.type};base64`:"base64",t=await s.arrayBuffer(),r=new Uint8Array(t),a=[];for(let n=0;n<r.length;n+=8192)a.push(String.fromCharCode(...r.subarray(n,n+8192)));return`data:${e},${btoa(a.join(""))}`},ee=async(s,e=16777216)=>{if(Number.isInteger(s?.size)){if(Number.isInteger(e)&&s.size>e){let t=`Blob size (${s.size} bytes) exceeds max (${e} bytes).`;throw new DOMException(t,"NotReadableError")}}else return null;return Q&&globalThis.Buffer?be(s):globalThis.FileReader?Ee(s):we(s)};var Ue=Object.freeze(["href","origin","protocol","username","password","host","hostname","port","pathname","search","hash"]),Se=Object.freeze({RETURN_DOM:!1,RETURN_DOM_FRAGMENT:!1,RETURN_TRUSTED_TYPE:!1}),h=(s,e,t)=>{s&&console.warn(`[URLSanitizer Debug] ${e}`,t)},N=class s extends T{#e;static#t=null;static#s=null;static#n(e,t){if(!t.attrValue||!/^\s*data:/i.test(t.attrValue))return;let r=s.#t,a=s.#s;if(!r||!a)return;let n=URL.parse(t.attrValue);if(n&&n.protocol==="data:"){let o=t.attrValue;if(a.recurse||(a.recurse=new Set),a.recurse.has(o)){let c=`Circular Data URL detected and skipped: ${o}`;h(a.debug,c),t.attrValue="";return}a.nest++,a.recurse.add(o);try{let c=r.#a(o,{allow:["data"],deny:[],only:[],allowRelative:!1},a);t.attrValue=c||""}finally{a.nest--,a.recurse.delete(o)}}}static{S.setConfig(Se),S.addHook("uponSanitizeAttribute",s.#n)}constructor(){super(),this.#e=new Set(super.get())}#r(e,t,r,a,n){if(z.test(e))return!1;if(e.split("+").some(i=>w.test(i))||!A.test(e)){let i=`Failed to add scheme '${e}' in '${t}' list.`;return h(n.debug,i),!1}return a.set(e,!0),r.add(e),!0}#o(e,t){let r=e;try{r=decodeURIComponent(e)}catch{}let a=s.#t,n=s.#s;s.#t=this,s.#s=t;let o;try{o=t.domPurify.sanitize(r)}finally{s.#t=a,s.#s=n}o=Z(o);try{return encodeURI(o)}catch{return o}}#a(e,t,r){if(r.nest>16)throw new Error("Data URLs nested too deeply.");let{allowedSchemes:a,restrictScheme:n,schemeMap:o}=this.#i(t,r),{isDataUrl:c,isRelative:i,isVerified:l,scheme:d,schemeParts:m,urlObj:f,urlToSanitize:u}=this.#c(e,t.allowRelative,a,r);return!l||!this.#l(d,m,n,o,i)?null:c?this.#p(f,d,r):this.#m(u)}#i({allow:e,deny:t,only:r},a){let n=this.#e,o=!1,c=new Map(a.schemeMap);if(r.length){n=new Set;for(let i of r)p(i)&&this.#r(i.trim(),"only",n,c,a)&&!o&&(o=!0)}else{if(e.length){n=new Set(this.#e);for(let i of e)p(i)&&this.#r(i.trim(),"allow",n,c,a)}if(t.length)for(let i of t)p(i)&&(i=i.trim(),i&&c.set(i,!1))}return{allowedSchemes:n,restrictScheme:o,schemeMap:c}}#c(e,t,r,a){let n=super.verify(e,r),o=!1,c="";if(!n&&t&&!V.test(e)){let m=e.normalize("NFKC"),f=URL.parse(e,"http://dummy.local"),u=URL.parse(m,"http://dummy.local");f&&u?f.protocol==="http:"&&f.hostname==="dummy.local"&&u.protocol==="http:"&&(n=!0,o=!0,c=`${f.pathname}${f.search}${f.hash}`):h(a.debug,"Failed to parse relative URL.")}if(!n)return{isVerified:!1};if(o)return{isRelative:o,isVerified:n,isDataUrl:!1,scheme:"",schemeParts:[],urlToSanitize:c};let i=URL.parse(e),l=i.protocol.replace(/:$/,"").normalize("NFKC"),d=l.split("+");return{isRelative:o,isVerified:n,scheme:l,schemeParts:d,urlObj:i,isDataUrl:d.includes("data"),urlToSanitize:i.href}}#l(e,t,r,a,n){if(n)return!0;if(r)return t.every(o=>a.get(o));for(let[o,c]of a.entries())if(!(c||e!==o&&t.every(l=>l!==o)))return!1;return!0}#p(e,t,r){let[a,...n]=e.pathname.split(","),o=`${n.join(",")}${e.search}${e.hash}`,c=a.split(";"),i=c[c.length-1]==="base64",l=o;if(i)try{l=W(o)}catch(d){return h(r.debug,"Failed to parse base64 data.",d),null}try{let m=J(l).trim().normalize("NFKC"),u=URL.parse(m,"http://dummy.local");if(!u)return h(r.debug,"Failed to parse inner data URL protocol."),null;if(u.protocol.replace(/:$/,"").split("+").some(x=>z.test(x)))return null}catch(d){return h(r.debug,"Failed to parse inner data URL protocol.",d),null}return(!a||G.test(a))&&(l=this.#o(l,r)),l?(i&&l!==o&&c.pop(),`${t}:${c.join(";")},${l}`):null}#m(e){let t=e;if(v.test(t)){let{index:r}=v.exec(t);t=t.substring(0,r).replace(/[?&]$/,"")}if(I.test(t)){let{index:r}=I.exec(t);t=t.substring(0,r).replace(/[?&]$/,"")}return t}sanitize(e,t){if(!e||!p(e))return null;let r=Number.isInteger(t?.maxLength)&&t.maxLength?t.maxLength:0;if(r&&e.length>r){let c=`URL length ${e.length} exceeds maxLength ${r}.`;throw new RangeError(c)}if(!(t&&(Array.isArray(t.deny)&&t.deny.length>0||Array.isArray(t.only)&&t.only.length>0||t.allowRelative))&&(e.startsWith("https://")||e.startsWith("http://"))&&!v.test(e)&&!e.includes("data:")){let c=URL.parse(e);return c?c.href.replace(/%26/g,K):(h(t?.debug,"Failed to parse URL."),null)}let n={allow:Array.isArray(t?.allow)?t.allow:[],deny:Array.isArray(t?.deny)?t.deny:[],only:Array.isArray(t?.only)?t.only:[],allowRelative:!!t?.allowRelative},o={debug:!!t?.debug,domPurify:S,nest:0,recurse:null,schemeMap:new Map([["blob",!1],["data",!1],["file",!1],["javascript",!1],["vbscript",!1]])};return this.#a(e,n,o)}inspect(e,t){if(!p(e))throw new TypeError(`Expected String but got ${R(e)}.`);let r={input:e,valid:!1},a,n=null,o=Number.isInteger(t?.maxLength)&&t.maxLength?t.maxLength:0,c=e.length;if(o&&c>o)n=`URL length ${c} exceeds maxLength ${o}.`;else if(this.verify(e)){let{protocol:i}=new URL(e);if(i==="blob:")a=e;else try{a=this.sanitize(e,t??{allow:["data"]}),a||(n="Sanitization failed (blocked by allowed schemes or rules).")}catch(l){n=l.message}}else n="Invalid URI syntax or scheme is not registered.";if(a){let i=new URL(a),{pathname:l,protocol:d}=i,f=d.replace(/:$/,"").split("+").includes("data");if(r.valid=!0,f){let[u,...E]=l.split(","),O=`${E.join(",")}${i.search}${i.hash}`,x=u.split(";"),M=x[x.length-1]==="base64";M&&x.pop(),r.data={mime:x.join(";"),base64:M,data:O}}else r.data=null;for(let u of Ue){let E=i[u];p(E)&&(r[u]=E)}}else r.valid=!1,n&&(r.reason=n);return r}get(){return[...this.#e]}has(e){return p(e)?this.#e.has(e.trim().toLowerCase()):!1}add(e){let t;if(p(e))t=e.trim().toLowerCase();else throw new TypeError(`Expected String but got ${R(e)}.`);if(t.split("+").some(n=>w.test(n))||!A.test(t))throw new Error(`Invalid scheme: ${e}`);return this.#e.add(t),[...this.#e]}remove(e){return p(e)?this.#e.delete(e.trim().toLowerCase()):!1}reset(){this.#e=new Set(super.get())}verify(e,t=this.#e){return super.verify(e,t)}};var y=new N,ve=async(s,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,revokeObjectURL:!1,maxBlobSize:16777216})=>{let t=!!e?.debug,r=Number.isInteger(e?.maxBlobSize)&&e.maxBlobSize>0?e.maxBlobSize:16777216,a;if(s&&p(s)){let n,o=URL.parse(s);if(o?n=o.protocol.replace(/:$/,""):h(t,`Invalid URL input format: ${s}`),n==="blob"){let{allow:c,deny:i,only:l}=e;if(Array.isArray(c)&&c.includes("blob")&&!(Array.isArray(i)&&i.includes("blob"))||Array.isArray(l)&&l.includes("blob")){let d;try{d=await fetch(s).then(m=>m.blob()).then(m=>ee(m,r))}catch(m){let f=`Failed to fetch and convert blob URL: ${s}`;h(t,f,m)}if(d){let m={...e};Array.isArray(e.only)?m.only=e.only.includes("data")?e.only:[...e.only,"data"]:Array.isArray(e.allow)&&(m.allow=e.allow.includes("data")?e.allow:[...e.allow,"data"],Array.isArray(e.deny)&&(m.deny=e.deny.filter(f=>f!=="data"))),a=y.sanitize(d,m)}}e?.revokeObjectURL&&URL.revokeObjectURL(s)}else(n||e.allowRelative)&&(a=y.sanitize(s,e))}return a||null},Le=(s,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,revokeObjectURL:!1})=>{let t=!!e?.debug,r;if(s&&p(s)){let a,n=URL.parse(s);if(n?a=n.protocol.replace(/:$/,""):h(t,`Invalid URL input format: ${s}`),a==="blob")return e?.revokeObjectURL&&URL.revokeObjectURL(s),null;(a||e.allowRelative)&&(r=y.sanitize(s,e))}return r||null},te=async s=>y.inspect(s),_e=async s=>te(s),se=s=>y.inspect(s),Ce=s=>se(s),Te=async s=>y.verify(s),$e=s=>y.verify(s),Ae=y;export{Ae as default,te as inspectURL,se as inspectURLSync,Te as isURI,$e as isURISync,_e as parseURL,Ce as parseURLSync,ve as sanitizeURL,Le as sanitizeURLSync};
|
|
1
|
+
var me=Object.freeze(["addHook","removeHook","removeAllHooks","setConfig"]),w=[],T=new Proxy({},{get(s,t,e){let r=globalThis.DOMPurify||globalThis.window?.DOMPurify;if(r){if(w.length){for(let{prop:o,args:n}of w)typeof r[o]=="function"&&r[o](...n);w.length=0}let i=r[t];return typeof i=="function"?i.bind(r):i}return(...i)=>{if(me.includes(t))return w.push({prop:t,args:i}),e;throw new Error("DOMPurify is not available. Ensure DOMPurify is exposed globally (e.g., window.DOMPurify).")}}});var h=s=>Object.prototype.toString.call(s).slice(8,-1),p=s=>typeof s=="string"||s instanceof String;var k=["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","aet","afp","afs","agtp","aim","amss","android","appdata","apt","ar","ari","ark","at","attachment","aw","barion","beshare","bitcoin","bitcoincash","bl","blob","bluetooth","bolo","brid","browserext","cabal","caip","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","cm","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cttps","cvs","dab","dat","data","dav","dhttp","diaspora","dict","did","dilithium3","dis","dlna-playcontainer","dlna-playsingle","dnp","dns","dntp","doi","donau","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","esim","ethereum","example","ez","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","gcx","gdsi","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","mdoc","mdoc-openid4vp","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","musik","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","nostr","notes","npamp","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","pkg","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","sss","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","tttps","turn","turns","tv","udp","unreal","upn","urn","ust","ut2004","uuaid","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"];var G=/#$/,j=/^(?:application\/(?:[\w.-]+\+)?xml|image\/svg\+xml|text\/(?:html|xml))(?:\s*;\s*|$)/i;var B=/&#(x0*[\dA-F]{1,2}|0*\d{1,3});?/gi,H=/^((?:[^#%]|%(?!23))*)\?$/,N=/^[a-z][\da-z+.-]*$/,F=/^(?:web\+[a-z]+|ext\+[a-z][\da-z.-]*)$/,y=/^(?:java|vb)script$/i,z=/^(?:(?:java|vb)script|blob)$/i,L=/["'<>]|%(?:22|27|3C|3E)|&(?:apos|lt|gt|quot|#0*(?:34|39|60|62)|#x0*(?:22|27|3C|3E));/i,X=/^%[\dA-F]{2}$/i,V=/(?:%[\dA-F]{2})+/gi,Y=/&/gi,q=/&(?=[#a-z0-9])(?![a-z0-9]+=)/i,Q=/^(?:\/{2,}|\\|[a-z][a-z\d+\-.]*:[^/])/i;var W=[9,10,13,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255];var $=new Set(W),A=new Map([[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),Ee=()=>{let s=new Map;for(let t=0;t<256;t++)!$.has(t)&&!A.has(t)&&s.set(t,`\\x${t.toString(16).padStart(2,"0").toUpperCase()}`);return s},O=Ee();var[g,ye,be,Re,xe,we]=["&","#","<",">",'"',"'"].map(s=>`%${s.charCodeAt(0).toString(16).toUpperCase()}`),Te={[g]:`${g}amp;`,[be]:`${g}lt;`,[Re]:`${g}gt;`,[xe]:`${g}quot;`,[we]:`${g}${ye}39;`},st=Array.from({length:256},(s,t)=>`%${t.toString(16).padStart(2,"0").toUpperCase()}`),_e=globalThis.process?.versions?.node!==void 0,K=`[${[...O.values()].join("")}]`,Se=new RegExp(K),Ue=new RegExp(K,"g"),rt=new TextEncoder,ve=new TextDecoder("utf-8",{fatal:!0}),U=class{#e=new Set(k);get(){return[...this.#e]}getScheme(t){if(!p(t))return null;let e=this.parse(t);return e?e.protocol.replace(/:$/,""):null}has(t){let e=this.normalize(t,!0);return e?this.#e.has(e):!1}normalize(t,e=!1){if(!p(t))return null;let r=t.normalize("NFKC");return e?r.trim().toLowerCase():r}parse(t,e){if(!p(t))return null;let r=this.normalize(t);return e&&p(e)?URL.parse(r,this.normalize(e)):URL.parse(r)}verify(t,e){if(!p(t))return!1;let r=this.parse(t);return this.verifyParsed(r,e)}verifyParsed(t,e){if(!t||!t.protocol)return!1;let r=t.protocol.toLowerCase().replace(/:$/,""),i=r.split("+");return i.some(n=>y.test(n))?!1:(e||(e=this.#e),e.has(r)||F.test(r)||i.every(n=>e.has(n)))}},I=s=>p(s)?s.replace(/:$/,"").split("+"):[];var J=s=>{if(!p(s))return s;let t=X.test(s)?s.toUpperCase():s;return Te[t]??s},E=s=>{let t=p(s)?s:String(s);return t.length>64?`${t.slice(0,64)}...`:t},ee=s=>p(s)?s.replace(G,"").replace(H,"$1"):s,Ce=(s,t)=>{let e=/^x/i.test(t)?Number.parseInt(t.slice(1),16):Number.parseInt(t,10);if(!Number.isInteger(e)||e<0||e>=256)return s;if(O.has(e))return"";if($.has(e))return String.fromCharCode(e);let r=A.get(e);return String.fromCodePoint(r)},te=(s,t=0)=>{if(!p(s))throw new TypeError(`Expected String but got ${h(s)}.`);if(!Number.isInteger(t))throw new TypeError(`Expected Number but got ${h(t)}.`);let e;try{e=decodeURIComponent(s)}catch{e=s.replace(V,i=>{try{return decodeURIComponent(i)}catch{return i}})}e=e.replace(Ue,"");let r=0;for(;r+t<16;r++){let i=e;if(e=e.replace(Y,"&").replace(B,Ce),e===i)break}if(r+t>=16&&/&#/.test(e))throw new Error("Character references nested too deeply.");return e},se=(s,t="",e="")=>{if(!p(s))return{mediaType:"",mediaTypes:[],data:"",isBase64:!1};let r=s.indexOf(",");if(r===-1)return{mediaType:s,mediaTypes:s.split(";"),data:"",isBase64:!1};let i=s.slice(0,r),o=`${s.slice(r+1)}${t}${e}`,n=i.split(";"),a=/^base64$/i.test(n.at(-1));return{mediaType:i,mediaTypes:n,data:o,isBase64:a}},re=s=>{if(!p(s))throw new TypeError(`Expected String but got ${h(s)}.`);let t=s.replace(/\s/g,""),e;try{e=atob(t)}catch{throw new Error(`Invalid base64 data: ${E(s)}`)}let r;if(_e&&globalThis.Buffer)r=globalThis.Buffer.from(e,"latin1");else{let i=e.length;r=new Uint8Array(i);for(let o=0;o<i;o++)r[o]=e.charCodeAt(o)}try{let i=ve.decode(r);return Se.test(i)?t:i}catch{return t}};var oe="http://dummy.local",ze=globalThis.process?.versions?.node!==void 0,Le=Object.freeze(["href","origin","protocol","username","password","host","hostname","port","pathname","search","hash"]),$e=Object.freeze({RETURN_DOM:!1,RETURN_DOM_FRAGMENT:!1,RETURN_TRUSTED_TYPE:!1}),v=Object.freeze({allow:Object.freeze([]),deny:Object.freeze([]),only:Object.freeze([]),allowRelative:!1,debug:!1,revokeObjectURL:!1,maxBlobSize:16777216,maxLength:65536}),C=(s,t)=>{console.warn(`[URLSanitizer Debug] ${s}`,t)},M=class{constructor(t,e){this.debug=!!t?.debug,this.domPurify=e,this.nest=0,this.recurse=new Set,this.schemeMap=new Map([["blob",!1],["data",!1],["file",!1],["javascript",!1],["vbscript",!1]])}enter(t){return this.recurse.has(t)?(this.debug&&C(`Circular Data URL detected and skipped: ${E(t)}`),!1):(this.nest++,this.recurse.add(t),!0)}leave(t){this.nest--,this.recurse.delete(t)}},D=class s extends U{#e;static#t=null;static#s=null;static#i(t,e){if(!e.attrValue||!/^\s*data:/i.test(e.attrValue))return;let r=s.#t,i=s.#s;if(!r||!i)return;let o=URL.parse(e.attrValue);if(o&&o.protocol==="data:"){let n=e.attrValue;if(!i.enter(n)){e.attrValue="";return}try{let a=r.#n(n,{allow:["data"],deny:[],only:[],allowRelative:!1},i);e.attrValue=a||""}finally{i.leave(n)}}}static{T.setConfig($e),T.addHook("uponSanitizeAttribute",s.#i)}constructor(){super(),this.#e=new Set(super.get())}#r(t,e,r,i,o){let n=this.normalize(t,!0);return z.test(n)||n.split("+").some(l=>y.test(l))||!N.test(n)?!1:(i.set(n,!0),r.add(n),!0)}#a(t,e){let r=t;try{r=decodeURIComponent(t)}catch{}let i=s.#t,o=s.#s;s.#t=this,s.#s=e;let n;try{n=e.domPurify.sanitize(r)}finally{s.#t=i,s.#s=o}n=ee(n);try{return encodeURI(n)}catch{return n}}#n(t,{allow:e,allowRelative:r,deny:i,only:o},n){if(n.nest>16)throw new Error("Data URLs nested too deeply.");let{allowedSchemes:a,restrictScheme:c,schemeMap:l}=this.#c({allow:e,deny:i,only:o},n),{isDataUrl:d,isRelative:m,isVerified:R,scheme:u,schemeParts:x,urlObj:pe,urlToSanitize:de}=this.#l(t,r,a,n);return!R||!this.#p(u,x,c,l,m)?null:d?this.#d(pe,u,n):this.#m(de)}#c({allow:t,deny:e,only:r},i){let o=this.#e,n=!1,a=new Map(i.schemeMap);if(Array.isArray(r)&&r.length){o=new Set;for(let c of r)p(c)&&this.#r(c.trim(),"only",o,a,i)&&!n&&(n=!0)}else{if(Array.isArray(t)&&t.length){o=new Set(this.#e);for(let c of t)p(c)&&this.#r(c,"allow",o,a,i)}if(Array.isArray(e)&&e.length){for(let c of e)if(p(c)){let l=this.normalize(c,!0);l&&a.set(l,!1)}}}return{allowedSchemes:o,restrictScheme:n,schemeMap:a}}#l(t,e,r,i){let o=this.parse(t),n=this.verifyParsed(o,r),a=!1,c="";if(!n&&e&&!Q.test(t)){let m=this.parse(t,oe);m&&m.protocol==="http:"&&m.hostname==="dummy.local"&&(n=!0,a=!0,c=`${m.pathname}${m.search}${m.hash}`)}if(!n)return{isVerified:!1};if(a)return{isRelative:a,isVerified:n,isDataUrl:!1,scheme:"",schemeParts:[],urlToSanitize:c};let l=o.protocol.replace(/:$/,""),d=l.split("+");return{isRelative:a,isVerified:n,scheme:l,schemeParts:d,urlObj:o,isDataUrl:l==="data",urlToSanitize:o.href}}#p(t,e,r,i,o){if(o)return!0;if(r)return i.has(t)||e.every(n=>i.get(n));for(let[n,a]of i.entries())if(!(a||t!==n&&e.every(l=>l!==n)))return!1;return!0}#d(t,e,r){let{mediaType:i,mediaTypes:o,data:n,isBase64:a}=se(t.pathname,t.search,t.hash),c=n;if(a)try{c=re(n)}catch(l){return r.debug&&C("Failed to parse base64 data.",l),null}try{let l=te(c).trim(),d=this.parse(l,oe);if(!d)return null;let m=d.protocol;if(I(m).some(u=>z.test(u)))return null}catch(l){return r.debug&&C("Failed to parse inner data URL protocol.",l),null}return(!i||j.test(i))&&(c=this.#a(c,r)),c?(a&&c!==n&&o.pop(),`${e}:${o.join(";")},${c}`):null}#m(t){let e=t,r=[L,q];for(let i of r){let o=i.exec(e);o&&(e=e.substring(0,o.index).replace(/[?&]$/,""))}return e}#o(t,e){if(!t||!p(t))return null;let{allow:r,allowRelative:i,deny:o,maxLength:n,only:a}=e;if(Number.isInteger(n)&&t.length>n){let d=`URL length ${t.length} exceeds max length ${n}.`;throw new RangeError(d)}if(!(Array.isArray(o)&&o.length||Array.isArray(a)&&a.length||i)&&(t.startsWith("https://")||t.startsWith("http://"))&&!L.test(t)&&!t.includes("data:")){let d=URL.parse(t);return d?d.href.replace(/%26/g,J):null}let l=new M(e,T);return this.#n(t,{allow:r,allowRelative:i,deny:o,only:a},l)}sanitize(t,e={}){let r={...v,...e};return this.#o(t,r)}inspect(t){let e={input:t,valid:!1};if(!p(t))return e.reason=`Invalid URL input: ${E(t)}`,e;if(t==="")return e.reason="Invalid URL input: (empty string)",e;let r,i=null;try{r=this.#o(t,{...v,allow:["data"],allowRelative:!0}),r||(i="Sanitization failed (blocked by allowed schemes or rules).")}catch(o){i=o.message}if(r){let o=URL.parse(r);if(e.valid=!0,o){let{pathname:n,protocol:a}=o;if(I(a).includes("data")){let[d,...m]=n.split(","),R=`${m.join(",")}${o.search}${o.hash}`,u=d.split(";"),x=u[u.length-1]==="base64";x&&u.pop(),e.data={mime:u.join(";"),base64:x,data:R}}else e.data=null;for(let d of Le){let m=o[d];p(m)&&(e[d]=m)}}else e.data=null,e.href=r}else e.valid=!1,e.reason=i;return e}get(){return[...this.#e]}has(t){let e=this.normalize(t,!0);return e?this.#e.has(e):!1}add(t){if(!p(t))throw new TypeError(`Expected String but got ${h(t)}.`);let e=this.normalize(t,!0);if(e.split("+").some(o=>y.test(o))||!N.test(e))throw new Error(`Invalid scheme: ${t}`);return this.#e.add(e),[...this.#e]}remove(t){if(!p(t))return!1;let e=this.normalize(t,!0);return this.#e.delete(e)}reset(){this.#e=new Set(super.get())}verify(t,e){return super.verify(t,e??this.#e)}},Ae=async s=>{let t=s.type?`${s.type};base64`:"base64",e=await s.arrayBuffer(),r=globalThis.Buffer.from(e).toString("base64");return`data:${t},${r}`},Oe=s=>new Promise((t,e)=>{let r=new globalThis.FileReader;r.addEventListener("error",()=>{let i=r.error||new DOMException("Failed to read Blob via FileReader.","NotReadableError");e(i)}),r.addEventListener("abort",()=>t(null)),r.addEventListener("load",()=>t(r.result)),r.readAsDataURL(s)}),Pe=async s=>{let t=s.type?`${s.type};base64`:"base64",e=await s.arrayBuffer(),r=new Uint8Array(e),i=[];for(let o=0;o<r.length;o+=8192)i.push(String.fromCharCode(...r.subarray(o,o+8192)));return`data:${t},${btoa(i.join(""))}`},ae=async(s,t,e,r=0)=>{let{done:i,value:o}=await s.read();if(i)return;let n=r+o.byteLength;if(n>t){await s.cancel("Size limit exceeded");let a=`Payload (${n} bytes) exceeds max (${t} bytes).`;throw new DOMException(a,"NotReadableError")}return e.push(o),ae(s,t,e,n)},ce=async(s,t)=>{let e=16777216;Number.isInteger(t)&&t>0&&(e=t);let r=await fetch(s);if(!r.ok){let a=`Failed to fetch ${E(s)}`;throw Number.isInteger(r.status)&&(r.statusText?a+=`: ${r.status} ${r.statusText}`:a+=`: ${r.status}`),new Error(a)}let i=r.headers.get("content-length");if(i){let n=Number.parseInt(i,10);if(Number.isInteger(n)&&n>e){let a=`Payload (${n} bytes) exceeds max (${e} bytes).`;throw new DOMException(a,"NotReadableError")}}let o;if(r.body){let n=r.body.getReader(),a=[];try{await ae(n,e,a,0)}finally{n.releaseLock()}let c=r.headers.get("content-type");c?o=new Blob(a,{type:c}):o=new Blob(a)}else if(o=await r.blob(),o.size>e){let n=`Payload (${o.size} bytes) exceeds max (${e} bytes).`;throw new DOMException(n,"NotReadableError")}return ze&&globalThis.Buffer?Ae(o):globalThis.FileReader?Oe(o):Pe(o)},f=new D,Ie=async(s,t={})=>{if(!s||!p(s))return null;let e={...v,...t},r=f.getScheme(s);if(r===null)return e.allowRelative?f.sanitize(s,e):null;if(r==="blob"){let{allow:i,deny:o,only:n}=e,a=null;if(i.includes("blob")&&!o.includes("blob")||n.includes("blob")){let c;try{c=await ce(s,e.maxBlobSize)}catch(l){e.debug&&C(`Failed to fetch and convert blob URL: ${E(s)}`,l)}c&&(n.length?n.includes("data")||(e.only=[...n,"data"]):(i.includes("data")||(e.allow=[...i,"data"]),e.deny=e.deny.filter(l=>l!=="data")),a=f.sanitize(c,e))}return e.revokeObjectURL&&URL.revokeObjectURL(s),a}return f.sanitize(s,e)},Me=(s,t={})=>{if(!s||!p(s))return null;let e={...v,...t},r=f.getScheme(s);return r===null?e.allowRelative?f.sanitize(s,e):null:r==="blob"?(e.revokeObjectURL&&URL.revokeObjectURL(s),null):f.sanitize(s,e)},De=async s=>{if(p(s)){let t=URL.parse(s);if(t?.protocol==="blob:")try{let e=await ce(t.href),r=f.inspect(e);return r.input=s,r}catch(e){return{input:s,valid:!1,reason:e.message}}}return f.inspect(s)},le=s=>f.verify(s),ke=s=>le(s),Ge=f;export{Ge as default,De as inspectURL,ke as isURISync,le as isValidURI,Ie as sanitizeURL,Me as sanitizeURLSync};
|
|
2
2
|
/*!
|
|
3
3
|
* URL Sanitizer
|
|
4
4
|
*
|