url-sanitizer 5.0.0 → 5.1.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 +20 -15
- package/dist/url-sanitizer-wo-dompurify.min.js +1 -1
- package/dist/url-sanitizer-wo-dompurify.min.js.map +4 -4
- package/dist/url-sanitizer.min.js +4 -4
- package/dist/url-sanitizer.min.js.map +4 -4
- package/package.json +8 -11
- package/src/index.js +70 -8
- package/src/lib/iana/uri-schemes.json +1 -1
- package/src/mjs/common.js +10 -0
- package/src/mjs/constant.js +6 -0
- package/src/mjs/filter.js +570 -0
- package/src/mjs/sanitize.js +499 -0
- package/src/mjs/scheme.js +149 -0
- package/src/mjs/text-chars.js +4 -4
- package/src/mjs/{uri-util.js → utility.js} +69 -119
- package/types/index.d.ts +26 -1
- package/types/mjs/common.d.ts +1 -0
- package/types/mjs/constant.d.ts +1 -0
- package/types/mjs/filter.d.ts +34 -0
- package/types/mjs/sanitize.d.ts +57 -0
- package/types/mjs/scheme.d.ts +11 -0
- package/types/mjs/{uri-util.d.ts → utility.d.ts} +10 -11
- package/src/mjs/sanitizer.js +0 -1059
- package/types/mjs/sanitizer.d.ts +0 -88
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ Sanitizes the given URL asynchronously.
|
|
|
112
112
|
* opt.maxBlobSize **\[number\]** Maximum allowed blob size in bytes. Default is `16_777_216` (16MB).
|
|
113
113
|
* opt.maxLength **\[number\]** Maximum allowed URL length. Default is `65_536` (64KB).
|
|
114
114
|
|
|
115
|
-
**Returns** **Promise<string?>** Sanitized URL,
|
|
115
|
+
**Returns** **Promise<string?>** Sanitized URL, or null.
|
|
116
116
|
|
|
117
117
|
#### Samples
|
|
118
118
|
|
|
@@ -238,30 +238,34 @@ Sanitizes the given URL and returns its parsed components.
|
|
|
238
238
|
#### InspectedURLResult
|
|
239
239
|
|
|
240
240
|
An object extending the standard [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API with additional properties.
|
|
241
|
-
The properties except for `input` and `
|
|
241
|
+
The properties except for `input`, `valid`, and `href` are omitted from the object for invalid URLs.
|
|
242
242
|
|
|
243
243
|
* `input` — **string** The original URL input.
|
|
244
244
|
* `valid` — **boolean** Indicates whether the URI is valid.
|
|
245
|
+
* `href` — **string?** The sanitized URL, or null.
|
|
246
|
+
* `relative` — **[boolean]** Indicates whether the input is a valid relative URL.
|
|
245
247
|
* `reason` — **[string]** The reason why the URL is invalid. Omitted if `valid` is `true`.
|
|
246
248
|
* `data` — **[{ mime: string; base64: boolean; data: string; } | null | undefined]** The parsed result of a data URL. Returns `null` if the URL is valid but not a data URL, and is omitted if the URL is invalid.
|
|
247
249
|
* `data.mime` — **string** The MIME type of the data.
|
|
248
250
|
* `data.base64` — **boolean** Indicates whether the data is base64-encoded.
|
|
249
251
|
* `data.data` — **string** The actual data part of the data URL.
|
|
250
|
-
* `
|
|
252
|
+
* `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, `hash` — **[string | undefined]** Properties identical to the standard URL API (omitted if the URL is invalid).
|
|
251
253
|
|
|
252
254
|
``` javascript
|
|
253
255
|
const res1 = await inspectURL('javascript:alert(1)');
|
|
254
256
|
/* => {
|
|
255
257
|
input: 'javascript:alert(1)',
|
|
256
|
-
valid: false
|
|
258
|
+
valid: false,
|
|
259
|
+
href: null,
|
|
260
|
+
reason: 'Sanitization failed (blocked by allowed schemes or rules).'
|
|
257
261
|
} */
|
|
258
262
|
|
|
259
263
|
const res2 = await inspectURL('https://www.example.com/?foo=bar#baz');
|
|
260
264
|
/* => {
|
|
261
265
|
input: 'https://www.example.com/?foo=bar#baz',
|
|
262
266
|
valid: true,
|
|
263
|
-
data: null,
|
|
264
267
|
href: 'https://www.example.com/?foo=bar#baz',
|
|
268
|
+
data: null,
|
|
265
269
|
origin: 'https://www.example.com',
|
|
266
270
|
protocol: 'https:',
|
|
267
271
|
hostname: 'www.example.com',
|
|
@@ -276,12 +280,12 @@ const res3 = await inspectURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJ
|
|
|
276
280
|
/* => {
|
|
277
281
|
input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==',
|
|
278
282
|
valid: true,
|
|
283
|
+
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
279
284
|
data: {
|
|
280
285
|
mime: 'image/svg+xml',
|
|
281
286
|
base64: false,
|
|
282
287
|
data: '%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E'
|
|
283
288
|
},
|
|
284
|
-
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
285
289
|
origin: 'null',
|
|
286
290
|
protocol: 'data:',
|
|
287
291
|
pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
@@ -293,12 +297,12 @@ const res4 = await inspectURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAU
|
|
|
293
297
|
/* => {
|
|
294
298
|
input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
295
299
|
valid: true,
|
|
300
|
+
href: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
296
301
|
data: {
|
|
297
302
|
mime: 'image/png',
|
|
298
303
|
base64: true,
|
|
299
304
|
data: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
|
|
300
305
|
},
|
|
301
|
-
href: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
302
306
|
origin: 'null',
|
|
303
307
|
protocol: 'data:',
|
|
304
308
|
pathname: 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
@@ -313,12 +317,12 @@ const res5 = await inspectURL(url5);
|
|
|
313
317
|
/* => {
|
|
314
318
|
input: 'blob:nodedata:82ecc5a4-aea8-48d7-a407-64e2ef0913da',
|
|
315
319
|
valid: true,
|
|
320
|
+
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
316
321
|
data: {
|
|
317
322
|
mime: 'image/svg+xml',
|
|
318
323
|
base64: false,
|
|
319
324
|
data: '%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E'
|
|
320
325
|
},
|
|
321
|
-
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
322
326
|
origin: 'null',
|
|
323
327
|
protocol: 'data:',
|
|
324
328
|
pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
@@ -411,7 +415,7 @@ This library validates the *syntax and safe construction* of a URL, but it does
|
|
|
411
415
|
* **Server-Side Request Forgery (SSRF):** We do not check if a URL points to an internal IP address (e.g., `http://localhost`, `http://169.254.169.254`) or a malicious external server.
|
|
412
416
|
* **Phishing & Open Redirects:** A perfectly valid HTTP URL pointing to a phishing site (e.g., `https://evil-example.com/login`) will be considered safe. You must implement your own domain allow-listing if you need to restrict destinations.
|
|
413
417
|
* **General HTML Sanitization:** While we sanitize HTML/SVG *inside* data URLs, this library is not a general-purpose HTML body sanitizer. Do not use it to sanitize arbitrary user-generated DOM content.
|
|
414
|
-
* **Compromised Host Environment:** We assume the native `URL`
|
|
418
|
+
* **Compromised Host Environment:** We assume the native `URL` and `fetch` APIs are intact. We do not protect against runtime attacks where these global objects have been monkey-patched or tampered with. Additionally, when using the lightweight build (`url-sanitizer-wo-dompurify.min.js`), ensuring a secure and untampered global `DOMPurify` instance is the responsibility of the host environment.
|
|
415
419
|
|
|
416
420
|
## AI / LLM Application Security
|
|
417
421
|
|
|
@@ -435,12 +439,13 @@ Execution times were measured using [mitata](https://github.com/evanwashere/mita
|
|
|
435
439
|
### Benchmark Results
|
|
436
440
|
|
|
437
441
|
| 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) |
|
|
438
|
-
| :--- |
|
|
439
|
-
| **Normal
|
|
440
|
-
| **
|
|
441
|
-
| **
|
|
442
|
-
| **
|
|
443
|
-
| **
|
|
442
|
+
| :--- | :--- | :--- | :--- |
|
|
443
|
+
| **Normal HTTPS URL** | ~1.41 µs/iter | ~4.00 µs/iter | ~4.65 µs/iter |
|
|
444
|
+
| **HTTPS URL with XSS** | ~6.44 µs/iter<br>returns sanitized URL | ~4.50 µs/iter<br>returns encoded URL (not fully sanitized against XSS) | ~6.56 µs/iter<br>returns encoded URL (not fully sanitized against XSS) |
|
|
445
|
+
| **XSS Scheme URL** | ~5.78 µs/iter<br>returns `null` | ~1.51 µs/iter<br>returns `about:blank` | ~6.33 µs/iter<br>throws `Error` |
|
|
446
|
+
| **XSS Data URL** | ~393.24 µs/iter<br>returns sanitized data URL | ~2.34 µs/iter<br>returns `about:blank` | ~9.83 µs/iter<br>throws `Error` |
|
|
447
|
+
| **XSS Blob URL** | ~606.15 µs/iter<br>returns sanitized data URL | ~2.14 µs/iter<br>returns blob URL as-is | ~9.98 µs/iter<br>throws `Error` |
|
|
448
|
+
| **Invalid URL** | ~0.22 µs/iter<br>returns `null` | ~1.22 µs/iter<br>returns invalid URL as-is | ~20.50 µs/iter<br>throws `Error` |
|
|
444
449
|
|
|
445
450
|
### Characteristics & Trade-offs
|
|
446
451
|
|
|
@@ -1,4 +1,4 @@
|
|
|
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};
|
|
1
|
+
var I="http://dummy.local";var R=n=>Object.prototype.toString.call(n).slice(8,-1),p=n=>typeof n=="string"||n instanceof String,b=(n,e)=>{console.warn(`[URLSanitizer Debug] ${n}`,e)};var ye=Object.freeze(["addHook","removeHook","removeAllHooks","setConfig"]),T=[],L=new Proxy({},{get(n,e,t){let s=globalThis.DOMPurify||globalThis.window?.DOMPurify;if(s){if(T.length){for(let{prop:i,args:a}of T)typeof s[i]=="function"&&s[i](...a);T.length=0}let r=s[e];return typeof r=="function"?r.bind(s):r}return(...r)=>{if(ye.includes(e))return T.push({prop:e,args:r}),t;throw new Error("DOMPurify is not available. Ensure DOMPurify is exposed globally (e.g., window.DOMPurify).")}}});var X=/#$/,F=/^(?:application\/(?:[\w.-]+\+)?xml|image\/svg\+xml|text\/(?:html|xml))(?:\s*;\s*|$)/i;var Y=/&#(x0*[\dA-F]{1,2}|0*\d{1,3});?/gi,q=/^((?:[^#%]|%(?!23))*)\?$/,v=/^[a-z][\da-z+.-]*$/,Q=/^(?:web\+[a-z]+|ext\+[a-z][\da-z.-]*)$/,y=/^(?:java|vb)script$/i,w=/^(?:(?:java|vb)script|blob)$/i,k=/["'<>]|%(?:22|27|3C|3E)|&(?:apos|lt|gt|quot|#0*(?:34|39|60|62)|#x0*(?:22|27|3C|3E));/i,W=/^%[\dA-F]{2}$/i,Z=/(?:%[\dA-F]{2})+/gi,K=/&/gi,J=/&(?=[#a-z0-9])(?![a-z0-9]+=)/i,ee=/^(?:\/{2,}|\\|[a-z][a-z\d+\-.]*:[^/])/i;var te=[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 D=new Set(te),G=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]]),xe=()=>{let n=new Map;for(let e=0;e<256;e++)!D.has(e)&&!G.has(e)&&n.set(e,`\\x${e.toString(16).padStart(2,"0").toUpperCase()}`);return n},B=xe();var[E,we,Se,_e,Ue,Te]=["&","#","<",">",'"',"'"].map(n=>`%${n.charCodeAt(0).toString(16).toUpperCase()}`),Le={[E]:`${E}amp;`,[Se]:`${E}lt;`,[_e]:`${E}gt;`,[Ue]:`${E}quot;`,[Te]:`${E}${we}39;`},qe=Array.from({length:256},(n,e)=>`%${e.toString(16).padStart(2,"0").toUpperCase()}`),ne=globalThis.process?.versions?.node!==void 0,ie=`[${[...B.values()].join("")}]`,ve=new RegExp(ie),ze=new RegExp(ie,"g"),Qe=new TextEncoder,Ce=new TextDecoder("utf-8",{fatal:!0}),f=(n,e=!1)=>{if(!p(n))return null;let t=n.normalize("NFKC");return e?t.trim().toLowerCase().replace(/:$/,""):t},u=(n,e,t=!1)=>{if(!p(n)||n==="")return null;let s=n,r=e&&p(e)?e:"";if(t&&(s=f(s),r&&(r=f(r))),typeof URL.parse=="function")return r?URL.parse(s,r):URL.parse(s);try{return r?new URL(s,r):new URL(s)}catch{return null}},oe=n=>p(n)?n.replace(/:$/,"").split("+"):[];var ae=n=>{if(!p(n))return n;let e=W.test(n)?n.toUpperCase():n;return Le[e]??n},g=n=>{let e=p(n)?n:String(n);return e.length>64?`${e.slice(0,64)}...`:e},ce=n=>p(n)?n.replace(X,"").replace(q,"$1"):n,Ae=(n,e)=>{let t=/^x/i.test(e)?Number.parseInt(e.slice(1),16):Number.parseInt(e,10);if(!Number.isInteger(t)||t<0||t>=256)return n;if(B.has(t))return"";if(D.has(t))return String.fromCharCode(t);let s=G.get(t);return String.fromCodePoint(s)},le=(n,e=0)=>{if(!p(n))throw new TypeError(`Expected String but got ${R(n)}.`);if(!Number.isInteger(e))throw new TypeError(`Expected Number but got ${R(e)}.`);let t;try{t=decodeURIComponent(n)}catch{t=n.replace(Z,r=>{try{return decodeURIComponent(r)}catch{return r}})}t=t.replace(ze,"");let s=0;for(;s+e<16;s++){let r=t;if(t=t.replace(K,"&").replace(Y,Ae),t===r)break}if(s+e>=16&&/&#/.test(t))throw new Error("Character references nested too deeply.");return t},A=(n,e="",t="")=>{if(!p(n))return{mediaType:"",mediaTypes:[],data:"",isBase64:!1};let s=n.indexOf(",");if(s===-1)return{mediaType:n,mediaTypes:n.split(";"),data:"",isBase64:!1};let r=n.slice(0,s),i=`${n.slice(s+1)}${e}${t}`,a=r.split(";"),o=/^base64$/i.test(a.at(-1));return{mediaType:r,mediaTypes:a,data:i,isBase64:o}},pe=n=>{if(!p(n))throw new TypeError(`Expected String but got ${R(n)}.`);let e=n.replace(/\s/g,""),t;try{t=atob(e)}catch{throw new Error(`Invalid base64 data: ${g(n)}`)}let s;if(ne&&globalThis.Buffer)s=globalThis.Buffer.from(t,"latin1");else{let r=t.length;s=new Uint8Array(r);for(let i=0;i<r;i++)s[i]=t.charCodeAt(i)}try{let r=Ce.decode(s);return ve.test(r)?e:r}catch{return e}},O=n=>{if(ne&&globalThis.Buffer)return globalThis.Buffer.from(n).toString("base64");let e=new Uint8Array(n),t=[];for(let s=0;s<e.length;s+=8192)t.push(String.fromCharCode(...e.subarray(s,s+8192)));return btoa(t.join(""))};var H=Object.freeze({allow:Object.freeze([]),deny:Object.freeze([]),only:Object.freeze([]),allowRelative:!1,debug:!1,revokeObjectURL:!1,maxBlobSize:16777216,maxLength:65536}),_=class{constructor(e,t){this.debug=!!e?.debug,this.schemes=e?.schemes instanceof Set?e.schemes:new Set(e?.schemes||[]),this.domPurify=t,this.nest=0,this.recurse=new Set,this.schemeMap=new Map([["blob",!1],["data",!1],["file",!1],["javascript",!1],["vbscript",!1]])}enter(e){return this.recurse.has(e)?(this.debug&&b(`Circular Data URL detected and skipped: ${g(e)}`),!1):(this.nest++,this.recurse.add(e),!0)}leave(e){this.nest--,this.recurse.delete(e)}},$=class{#e(e,{allow:t,allowRelative:s,deny:r,only:i},a){if(a.nest>16)throw new Error("Data URLs nested too deeply.");let{allowedSchemes:o,restrictScheme:c,schemeMap:l}=this.#t({allow:t,deny:r,only:i},a),{isDataURL:m,isRelative:h,isVerified:d,scheme:U,schemeParts:ue,urlObj:he,urlToSanitize:ge}=this.#n(e,s,o,a);return!d||!this.#o(U,ue,c,l,h)?null:m?this.#i(he,U,a):this.#l(ge)}#t({allow:e,deny:t,only:s},r){let i=new Set(r.schemes),a=!1,o=new Map(r.schemeMap);if(Array.isArray(s)&&s.length){i=new Set,a=!0;for(let c of s)p(c)&&this.#s(c,i,o)}else{if(Array.isArray(e)&&e.length)for(let c of e)p(c)&&this.#s(c,i,o);if(Array.isArray(t)&&t.length){for(let c of t)if(p(c)){let l=f(c,!0);l&&o.set(l,!1)}}}return{allowedSchemes:i,restrictScheme:a,schemeMap:o}}#s(e,t,s){let r=f(e,!0);return this.#r(r)?(s.set(r,!0),t.add(r),!0):!1}#r(e){return w.test(e)?!1:!e.split("+").some(r=>y.test(r))&&v.test(e)}#n(e,t,s,r){let i=u(e,null,!0),a=s.has(f(i?.protocol,!0)),o=!1,c="";if(!a&&t&&!ee.test(e)){let m=u(e,I);m&&m.protocol==="http:"&&m.hostname==="dummy.local"&&(a=!0,o=!0,c=`${m.pathname}${m.search}${m.hash}`)}if(!a)return{isVerified:!1};if(o)return{isRelative:o,isVerified:a,isDataURL:!1,scheme:"",schemeParts:[],urlToSanitize:c};let l=f(i.protocol,!0);return{isRelative:o,isVerified:a,scheme:l,schemeParts:l.split("+"),urlObj:i,isDataURL:l==="data",urlToSanitize:i.href}}#o(e,t,s,r,i){if(i)return!0;if(s)return r.has(e);for(let[a,o]of r.entries())if(!o&&(e===a||t.includes(a)))return!1;return!0}#i(e,t,s){let{mediaType:r,mediaTypes:i,data:a,isBase64:o}=A(e.pathname,e.search,e.hash),c=a;if(o)try{c=pe(a)}catch(l){return s.debug&&b("Failed to parse base64 data.",l),null}try{let l=le(c).trim(),m=u(l,I);if(!m)return null;let h=m.protocol;if(oe(h).some(U=>w.test(U)))return null}catch(l){return s.debug&&b("Failed to parse inner data URL protocol.",l),null}return(!r||F.test(r))&&(c=this.#a(c,s)),c?(o&&c!==a&&i.pop(),`${t}:${i.join(";")},${c}`):null}#a(e,t){let s=e;try{s=decodeURIComponent(e)}catch{}let r,i=(a,o)=>this.#c(a,o,t);t.domPurify.addHook("uponSanitizeAttribute",i);try{r=t.domPurify.sanitize(s)}finally{t.domPurify.removeHook("uponSanitizeAttribute")}r=ce(r);try{return encodeURI(r)}catch{return r}}#c(e,t,s){if(!t.attrValue||!/^\s*data:/i.test(t.attrValue))return;let r=u(t.attrValue,null,!0);if(!r||r.protocol!=="data:")return;let i=t.attrValue;if(!s.enter(i)){t.attrValue="";return}try{let a=this.#e(i,{allow:["data"],deny:[],only:[],allowRelative:!1},s);t.attrValue=a||""}finally{s.leave(i)}}#l(e){let t=e,s=[k,J];for(let r of s){let i=r.exec(t);i&&(t=t.substring(0,i.index).replace(/[?&]$/,""))}return t}sanitize(e,t,s={}){if(!e||!p(e))return null;let r={...H,...s,schemes:t instanceof Set?t:new Set(t||[])},{allow:i,allowRelative:a,deny:o,maxLength:c,only:l}=r;if(Number.isInteger(c)&&e.length>c){let d=`URL length ${e.length} exceeds max length ${c}.`;throw new RangeError(d)}if(!(Array.isArray(o)&&o.length>0||Array.isArray(l)&&l.length>0||a)&&(e.startsWith("https://")||e.startsWith("http://"))&&!k.test(e)&&!e.includes("data:")){let d=u(e,null,!0);return d?d.href.replace(/%26/g,ae):null}let h=new _(r,L);return this.#e(e,{allow:i,allowRelative:a,deny:o,only:l},h)}async sanitizeBuffer(e,t,s,r={}){if(!(e instanceof ArrayBuffer))return null;let i={...H,...r,schemes:s instanceof Set?s:new Set(s||[])},a=new _(i,L),{schemeMap:o}=this.#t(i,a);if(!o.get("data"))return null;let c=O(e),l=`data:${t?`${t};base64`:"base64"},${c}`;if(Number.isInteger(i.maxLength)&&l.length>i.maxLength)throw new RangeError(`URL length ${l.length} exceeds max length ${i.maxLength}.`);let m=u(l);return m?this.#i(m,"data",a):null}async sanitizeDataURL(e,t,s={}){if(!e||!p(e))return null;let r={...H,...s,schemes:t instanceof Set?t:new Set(t||[])},{maxLength:i}=r;if(Number.isInteger(i)&&e.length>i)throw new RangeError(`URL length ${e.length} exceeds max length ${i}.`);let a=u(e);if(!a)return null;let o=f(a.protocol,!0);if(o!=="data")return null;let c=new _(r,L),{schemeMap:l}=this.#t(r,c);return l.get(o)?this.#i(a,o,c):null}};var me=["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","interaction","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 P=class{#e=new Set(me);get(){return[...this.#e]}getScheme(e){if(!p(e))return null;let t=this.parse(e);return t?this.normalize(t.protocol,!0):null}has(e){let t=this.normalize(e,!0);return t?this.#e.has(t):!1}normalize(e,t=!1){return f(e,t)}normalizeSchemes(e){if(Array.isArray(e)){let t=[];for(let s of e){let r=f(s,!0);r&&t.push(r)}return t}return[]}parse(e,t,s=!1){return u(e,t,s)}verifyURI(e,t){if(!p(e))return!1;let s=this.parse(e);return s?this.verifyScheme(s.protocol,t,!0):!1}verifyScheme(e,t,s=!1){if(!p(e))return!1;let r=this.normalize(e,!0),i=r.split("+");return i.some(o=>y.test(o))?!1:(t||(t=this.#e),t.has(r)?!0:i.length>1?s&&Q.test(r)||i.every(o=>t.has(o)):!1)}};var $e=Object.freeze(["href","origin","protocol","username","password","host","hostname","port","pathname","search","hash"]),fe=Object.freeze({allow:Object.freeze([]),deny:Object.freeze([]),only:Object.freeze([]),allowRelative:!1,debug:!1,revokeObjectURL:!1,maxBlobSize:16777216,maxLength:65536}),Ne=async(n,e)=>{let t=n.body.getReader(),s=[];try{let r=0;for(;r<=e;){let{done:o,value:c}=await t.read();if(o)break;if(r+=c.byteLength,r>e)throw await t.cancel("Size limit exceeded"),new DOMException(`Payload (${r} bytes) exceeds max (${e} bytes).`,"NotReadableError");s.push(c)}let i=new Uint8Array(r),a=0;for(let o of s)i.set(o,a),a+=o.byteLength;return i.buffer}finally{t.releaseLock()}},de=async(n,e)=>{let t=16777216;Number.isInteger(e)&&e>0&&(t=e);let s=await fetch(n);if(!s.ok){let c=`Failed to fetch ${g(n)}`;throw Number.isInteger(s.status)&&(s.statusText?c+=`: ${s.status} ${s.statusText}`:c+=`: ${s.status}`),new Error(c)}let r=s.headers.get("content-length");if(r){let o=Number.parseInt(r,10);if(Number.isInteger(o)&&o>t)throw new DOMException(`Payload (${o} bytes) exceeds max (${t} bytes).`,"NotReadableError")}let i=s.headers.get("content-type")||"",a;if(s.body)a=await Ne(s,t);else{let o=await s.blob();if(o.size>t)throw new DOMException(`Payload (${o.size} bytes) exceeds max (${t} bytes).`,"NotReadableError");a=await o.arrayBuffer()}return{buffer:a,mimeType:i}},M=class extends P{#e;#t;constructor(){super(),this.#e=new Set(super.get()),this.#t=new $}#s(e){return!p(e)||w.test(e)?!1:!e.split("+").some(r=>y.test(r))&&v.test(e)}#r(e,t={}){if(!e||!p(e))return{isValid:!1};let{allow:s,deny:r,only:i,...a}=t,o={...fe,...a,allow:this.normalizeSchemes(s),deny:this.normalizeSchemes(r),only:this.normalizeSchemes(i)},c=this.getScheme(e);return c===null&&!o.allowRelative?{isValid:!1}:{options:o,scheme:c}}#n(e){let t={input:e,href:null,valid:!1};if(!p(e))return t.reason=`Invalid URL input: ${g(e)}`,t;if(e==="")return t.reason="Invalid URL input: (empty string)",t;let s,r=null;try{s=this.#t.sanitize(e,[...this.#e],{...fe,allow:["data"],allowRelative:!0}),s||(r="Sanitization failed (blocked by allowed schemes or rules).")}catch(i){r=i.message}if(s){let i=u(s);if(t.valid=!0,i){if(f(i.protocol,!0)==="data"){let{mediaTypes:o,data:c,isBase64:l}=A(i.pathname,i.search,i.hash);l&&o.pop(),t.data={mime:o.join(";"),base64:l,data:c}}else t.data=null;for(let o of $e){let c=i[o];p(c)&&(t[o]=c)}}else t.valid=!1,t.relative=!0,t.href=s,t.reason="Invalid as an absolute URL, but valid as a relative URL."}else t.reason=r;return t}get(){return[...this.#e]}has(e){let t=f(e,!0);return t?this.#e.has(t):!1}add(e){if(!p(e))throw new TypeError(`Expected String but got ${R(e)}.`);let t=f(e,!0);if(!this.#s(t))throw new Error(`Invalid scheme: ${e}`);return this.#e.add(t),[...this.#e]}remove(e){if(!p(e))return!1;let t=f(e,!0);return this.#e.delete(t)}reset(){this.#e=new Set(super.get())}async sanitizeURL(e,t){let{isValid:s,options:r,scheme:i}=this.#r(e,t);if(s===!1)return null;if(i==="blob"){let{allow:a,deny:o,only:c}=r,l=null;if(a.includes("blob")&&!o.includes("blob")||c.includes("blob")){let m=null,h="";try{let d=await de(e,r.maxBlobSize);m=d.buffer,h=d.mimeType}catch(d){r.debug&&b(`Failed to fetch and convert blob URL: ${g(e)}`,d)}m&&(c.length?c.includes("data")||(r.only=[...c,"data"]):(a.includes("data")||(r.allow=[...a,"data"]),r.deny=o.filter(d=>d!=="data")),l=await this.#t.sanitizeBuffer(m,h,this.#e,r))}return r.revokeObjectURL&&URL.revokeObjectURL(e),l}else if(i==="data")return this.#t.sanitizeDataURL(e,this.#e,r);return this.#t.sanitize(e,this.#e,r)}sanitizeURLSync(e,t){let{isValid:s,options:r,scheme:i}=this.#r(e,t);return s===!1?null:i==="blob"?(r.revokeObjectURL&&URL.revokeObjectURL(e),null):this.#t.sanitize(e,this.#e,r)}async inspectURL(e){if(p(e)){let t=u(e,null,!0);if(t?.protocol==="blob:")try{let{buffer:s,mimeType:r}=await de(t.href),i=O(s),a=`data:${r?`${r};base64`:"base64"},${i}`,o=this.#n(a);return o.input=e,o}catch(s){return{input:e,valid:!1,reason:s.message}}}return this.#n(e)}isValidURI(e){return super.verifyURI(e,this.#e)}};var x=new M,Rt=async(n,e)=>x.sanitizeURL(n,e),bt=(n,e)=>x.sanitizeURLSync(n,e),yt=async n=>x.inspectURL(n),Et=n=>x.isValidURI(n),xt=n=>x.isValidURI(n),wt=x;export{wt as default,yt as inspectURL,xt as isURISync,Et as isValidURI,Rt as sanitizeURL,bt as sanitizeURLSync};
|
|
2
2
|
/*!
|
|
3
3
|
* URL Sanitizer
|
|
4
4
|
*
|