url-sanitizer 4.2.2 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -49
- 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 +20 -26
- 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 +559 -442
- package/src/mjs/text-chars.js +6 -6
- package/src/mjs/uri-util.js +201 -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 +20 -11
- 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
|
|
|
@@ -244,6 +242,7 @@ The properties except for `input` and `valid` are omitted from the object for in
|
|
|
244
242
|
|
|
245
243
|
* `input` — **string** The original URL input.
|
|
246
244
|
* `valid` — **boolean** Indicates whether the URI is valid.
|
|
245
|
+
* `relative` - **[boolean]** Indicates whether the input is a valid relative URL.
|
|
247
246
|
* `reason` — **[string]** The reason why the URL is invalid. Omitted if `valid` is `true`.
|
|
248
247
|
* `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.
|
|
249
248
|
* `data.mime` — **string** The MIME type of the data.
|
|
@@ -307,7 +306,6 @@ const res4 = await inspectURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAU
|
|
|
307
306
|
...
|
|
308
307
|
} */
|
|
309
308
|
|
|
310
|
-
// Note that blob URLs are parsed but not yet sanitized
|
|
311
309
|
const blob5 = new Blob(['<svg><g onload="alert(1)"/></svg>'], {
|
|
312
310
|
type: 'image/svg+xml'
|
|
313
311
|
});
|
|
@@ -316,22 +314,20 @@ const res5 = await inspectURL(url5);
|
|
|
316
314
|
/* => {
|
|
317
315
|
input: 'blob:nodedata:82ecc5a4-aea8-48d7-a407-64e2ef0913da',
|
|
318
316
|
valid: true,
|
|
319
|
-
data:
|
|
320
|
-
|
|
317
|
+
data: {
|
|
318
|
+
mime: 'image/svg+xml',
|
|
319
|
+
base64: false,
|
|
320
|
+
data: '%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E'
|
|
321
|
+
},
|
|
322
|
+
href: 'data:image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
321
323
|
origin: 'null',
|
|
322
|
-
protocol: '
|
|
323
|
-
pathname: '
|
|
324
|
+
protocol: 'data:',
|
|
325
|
+
pathname: 'image/svg+xml,%3Csvg%3E%3Cg%3E%3C/g%3E%3C/svg%3E',
|
|
324
326
|
...
|
|
325
327
|
} */
|
|
326
328
|
```
|
|
327
329
|
|
|
328
|
-
###
|
|
329
|
-
|
|
330
|
-
**Note:** `parseURLSync()` is deprecated. Please use `inspectURLSync()` instead.
|
|
331
|
-
|
|
332
|
-
Synchronous version of `inspectURL()`.
|
|
333
|
-
|
|
334
|
-
### isURI(uri)
|
|
330
|
+
### isValidURI(uri)
|
|
335
331
|
|
|
336
332
|
Checks if the given string is a valid URI and whether its scheme is registered in the allowed list.
|
|
337
333
|
|
|
@@ -339,39 +335,37 @@ Specifically, it verifies that the input has a correct URI syntax, is not a deni
|
|
|
339
335
|
* The scheme is registered in the default list (e.g., IANA registered schemes, `https`, `mailto`).
|
|
340
336
|
* The scheme has a custom prefix like `web+` or `ext+` (which are always allowed for web applications).
|
|
341
337
|
|
|
338
|
+
**Note:** `isURISync()` is deprecated. Please use `isValidURI()` instead.
|
|
339
|
+
|
|
342
340
|
#### Parameters
|
|
343
341
|
|
|
344
342
|
* uri **string** URI input.
|
|
345
343
|
|
|
346
|
-
**Returns** **
|
|
344
|
+
**Returns** **boolean** `true` if the URI is syntactically valid and uses an allowed/registered scheme; otherwise `false`.
|
|
347
345
|
|
|
348
346
|
* Always `true` for valid `web+*` and `ext+*` schemes (except `web+javascript`, `web+vbscript`, `ext+javascript`, and `ext+vbscript`).
|
|
349
347
|
* Always `false` for `javascript` and `vbscript` schemes, or any unknown/unregistered schemes (e.g., `foo:`).
|
|
350
348
|
|
|
351
349
|
``` javascript
|
|
352
|
-
const res1 =
|
|
350
|
+
const res1 = isValidURI('https://example.com/foo');
|
|
353
351
|
// => true
|
|
354
352
|
|
|
355
|
-
const res2 =
|
|
353
|
+
const res2 = isValidURI('javascript:alert(1)');
|
|
356
354
|
// => false
|
|
357
355
|
|
|
358
|
-
const res3 =
|
|
356
|
+
const res3 = isValidURI('mailto:foo@example.com');
|
|
359
357
|
// => true
|
|
360
358
|
|
|
361
|
-
const res4 =
|
|
359
|
+
const res4 = isValidURI('foo:bar');
|
|
362
360
|
// => false
|
|
363
361
|
|
|
364
|
-
const res5 =
|
|
362
|
+
const res5 = isValidURI('web+foo:bar');
|
|
365
363
|
// => true
|
|
366
364
|
|
|
367
|
-
const res6 =
|
|
365
|
+
const res6 = isValidURI('web+javascript:alert(1)');
|
|
368
366
|
// => false
|
|
369
367
|
```
|
|
370
368
|
|
|
371
|
-
### isURISync(uri)
|
|
372
|
-
|
|
373
|
-
Synchronous version of `isURI()`.
|
|
374
|
-
|
|
375
369
|
### urlSanitizer
|
|
376
370
|
|
|
377
371
|
The instance of the sanitizer.
|
|
@@ -418,7 +412,7 @@ This library validates the *syntax and safe construction* of a URL, but it does
|
|
|
418
412
|
* **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.
|
|
419
413
|
* **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.
|
|
420
414
|
* **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.
|
|
421
|
-
* **Compromised Host Environment:** We assume the native `URL`
|
|
415
|
+
* **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.
|
|
422
416
|
|
|
423
417
|
## AI / LLM Application Security
|
|
424
418
|
|
|
@@ -431,7 +425,7 @@ Because `url-sanitizer` performs **Deep Data URL Inspection** — decoding the p
|
|
|
431
425
|
|
|
432
426
|
### Neutralizing Hallucinated Schemes
|
|
433
427
|
LLMs generate URLs based on statistical linguistic patterns rather than factual databases.
|
|
434
|
-
As a result, they
|
|
428
|
+
As a result, they may generate hallucinate plausible-looking but non-existent or hazardous URI schemes (e.g., `ai-agent://`, `host-settings:`).
|
|
435
429
|
By operating on a whitelist approach, `url-sanitizer` automatically denies any unrecognized or unregistered protocols.
|
|
436
430
|
This strict blocking prevents non-existent or hallucinated schemes from inadvertently triggering application-specific or OS-level protocol hijacking.
|
|
437
431
|
|
|
@@ -443,22 +437,26 @@ Execution times were measured using [mitata](https://github.com/evanwashere/mita
|
|
|
443
437
|
|
|
444
438
|
| 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
439
|
| :--- | :---: | :---: | :---: |
|
|
446
|
-
| **Normal HTTP URL** | ~
|
|
447
|
-
| **XSS URL** | ~5.
|
|
448
|
-
| **
|
|
449
|
-
| **
|
|
440
|
+
| **Normal HTTP URL** | ~1.25 µs/iter | ~4.33 µs/iter | ~4.68 µs/iter |
|
|
441
|
+
| **XSS URL** | ~5.68 µs/iter | ~1.63 µs/iter | ~9.44 µs/iter |
|
|
442
|
+
| **Data URL** | ~371.03 µs/iter | ~2.85 µs/iter | ~9.44 µs/iter |
|
|
443
|
+
| **Blob URL** | ~552.58 µs/iter | ~2.18 µs/iter | ~9.11 µs/iter |
|
|
444
|
+
| **Invalid URL** | ~0.28 µs/iter | ~1.35 µs/iter | ~21.22 µs/iter |
|
|
450
445
|
|
|
451
446
|
### Characteristics & Trade-offs
|
|
452
447
|
|
|
453
448
|
* **Optimized for Standard Routing & XSS Rejection**
|
|
454
|
-
* For **
|
|
449
|
+
* 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
450
|
* **Exceptional Invalid URL Handling (Fail-Fast)**
|
|
456
|
-
* For **
|
|
451
|
+
* 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
452
|
* **Deep Inspection for High-Risk Payloads**
|
|
458
|
-
* For **
|
|
459
|
-
1.
|
|
460
|
-
2.
|
|
461
|
-
3.
|
|
453
|
+
* For **Data URLs** and **Blob URLs**, `url-sanitizer` prioritizes security over raw execution speed. The following inspection pipeline is applied:
|
|
454
|
+
1. If Blob URL is given, convert it to a Data URL.
|
|
455
|
+
2. Parses and normalizes the Data URL structure.
|
|
456
|
+
3. Decodes Base64 payloads when present.
|
|
457
|
+
4. Decodes URL-encoded content and numeric character references.
|
|
458
|
+
5. Sanitizes embedded HTML/SVG content using `DOMPurify`.
|
|
459
|
+
6. Re-encodes the purified payload into a safe URL representation.
|
|
462
460
|
|
|
463
461
|
## Acknowledgments
|
|
464
462
|
|
|
@@ -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"]),b=[],I=new Proxy({},{get(r,t,e){let s=globalThis.DOMPurify||globalThis.window?.DOMPurify;if(s){if(b.length){for(let{prop:o,args:i}of b)typeof s[o]=="function"&&s[o](...i);b.length=0}let n=s[t];return typeof n=="function"?n.bind(s):n}return(...n)=>{if(me.includes(t))return b.push({prop:t,args:n}),e;throw new Error("DOMPurify is not available. Ensure DOMPurify is exposed globally (e.g., window.DOMPurify).")}}});var u=r=>Object.prototype.toString.call(r).slice(8,-1),p=r=>typeof r=="string"||r instanceof String;var M=["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 D=/#$/,G=/^(?:application\/(?:[\w.-]+\+)?xml|image\/svg\+xml|text\/(?:html|xml))(?:\s*;\s*|$)/i;var H=/&#(x0*[\dA-F]{1,2}|0*\d{1,3});?/gi,B=/^((?:[^#%]|%(?!23))*)\?$/,j=/^[a-z][\da-z+.-]*$/,F=/^(?:web\+[a-z]+|ext\+[a-z][\da-z.-]*)$/,x=/^(?:java|vb)script$/i,v=/^(?:(?:java|vb)script|blob)$/i,U=/["'<>]|%(?: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 C=new Set(W),z=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 r=new Map;for(let t=0;t<256;t++)!C.has(t)&&!z.has(t)&&r.set(t,`\\x${t.toString(16).padStart(2,"0").toUpperCase()}`);return r},L=Ee();var[h,ye,be,xe,Re,we]=["&","#","<",">",'"',"'"].map(r=>`%${r.charCodeAt(0).toString(16).toUpperCase()}`),_e={[h]:`${h}amp;`,[be]:`${h}lt;`,[xe]:`${h}gt;`,[Re]:`${h}quot;`,[we]:`${h}${ye}39;`},tt=Array.from({length:256},(r,t)=>`%${t.toString(16).padStart(2,"0").toUpperCase()}`),Se=globalThis.process?.versions?.node!==void 0,K=`[${[...L.values()].join("")}]`,Te=new RegExp(K),ve=new RegExp(K,"g"),st=new TextEncoder,Ue=new TextDecoder("utf-8",{fatal:!0}),_=class{#e=new Set(M);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 s=t.normalize("NFKC");return e?s.trim().toLowerCase():s}parse(t,e){if(!p(t))return null;let s=this.normalize(t);return e&&p(e)?URL.parse(s,this.normalize(e)):URL.parse(s)}verify(t,e){if(!p(t))return!1;let s=this.parse(t);return this.verifyParsed(s,e,!0)}verifyParsed(t,e,s=!1){if(!t||!t.protocol)return!1;let n=t.protocol.toLowerCase().replace(/:$/,""),o=n.split("+");return o.some(a=>x.test(a))?!1:(e||(e=this.#e),e.has(n)?!0:o.length>1?s&&F.test(n)||o.every(a=>e.has(a)):!1)}},$=r=>p(r)?r.replace(/:$/,"").split("+"):[];var J=r=>{if(!p(r))return r;let t=X.test(r)?r.toUpperCase():r;return _e[t]??r},g=r=>{let t=p(r)?r:String(r);return t.length>64?`${t.slice(0,64)}...`:t},ee=r=>p(r)?r.replace(D,"").replace(B,"$1"):r,Ce=(r,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 r;if(L.has(e))return"";if(C.has(e))return String.fromCharCode(e);let s=z.get(e);return String.fromCodePoint(s)},te=(r,t=0)=>{if(!p(r))throw new TypeError(`Expected String but got ${u(r)}.`);if(!Number.isInteger(t))throw new TypeError(`Expected Number but got ${u(t)}.`);let e;try{e=decodeURIComponent(r)}catch{e=r.replace(V,n=>{try{return decodeURIComponent(n)}catch{return n}})}e=e.replace(ve,"");let s=0;for(;s+t<16;s++){let n=e;if(e=e.replace(Y,"&").replace(H,Ce),e===n)break}if(s+t>=16&&/&#/.test(e))throw new Error("Character references nested too deeply.");return e},A=(r,t="",e="")=>{if(!p(r))return{mediaType:"",mediaTypes:[],data:"",isBase64:!1};let s=r.indexOf(",");if(s===-1)return{mediaType:r,mediaTypes:r.split(";"),data:"",isBase64:!1};let n=r.slice(0,s),o=`${r.slice(s+1)}${t}${e}`,i=n.split(";"),a=/^base64$/i.test(i.at(-1));return{mediaType:n,mediaTypes:i,data:o,isBase64:a}},se=r=>{if(!p(r))throw new TypeError(`Expected String but got ${u(r)}.`);let t=r.replace(/\s/g,""),e;try{e=atob(t)}catch{throw new Error(`Invalid base64 data: ${g(r)}`)}let s;if(Se&&globalThis.Buffer)s=globalThis.Buffer.from(e,"latin1");else{let n=e.length;s=new Uint8Array(n);for(let o=0;o<n;o++)s[o]=e.charCodeAt(o)}try{let n=Ue.decode(s);return Te.test(n)?t:n}catch{return t}};var ne="http://dummy.local",Le=globalThis.process?.versions?.node!==void 0,Ne=Object.freeze(["href","origin","protocol","username","password","host","hostname","port","pathname","search","hash"]),S=Object.freeze({allow:Object.freeze([]),deny:Object.freeze([]),only:Object.freeze([]),allowRelative:!1,debug:!1,revokeObjectURL:!1,maxBlobSize:16777216,maxLength:65536}),T=(r,t)=>{console.warn(`[URLSanitizer Debug] ${r}`,t)},O=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&&T(`Circular Data URL detected and skipped: ${g(t)}`),!1):(this.nest++,this.recurse.add(t),!0)}leave(t){this.nest--,this.recurse.delete(t)}},P=class extends _{#e;constructor(){super(),this.#e=new Set(super.get())}#t(t,e){if(!t||!p(t))return null;let{allow:s,allowRelative:n,deny:o,maxLength:i,only:a}=e;if(Number.isInteger(i)&&t.length>i){let d=`URL length ${t.length} exceeds max length ${i}.`;throw new RangeError(d)}if(!(Array.isArray(o)&&o.length||Array.isArray(a)&&a.length||n)&&(t.startsWith("https://")||t.startsWith("http://"))&&!U.test(t)&&!t.includes("data:")){let d=this.parse(t);return d?d.href.replace(/%26/g,J):null}let l=new O(e,I);return this.#s(t,{allow:s,allowRelative:n,deny:o,only:a},l)}#s(t,{allow:e,allowRelative:s,deny:n,only:o},i){if(i.nest>16)throw new Error("Data URLs nested too deeply.");let{allowedSchemes:a,restrictScheme:c,schemeMap:l}=this.#o({allow:e,deny:n,only:o},i),{isDataUrl:d,isRelative:f,isVerified:k,scheme:y,schemeParts:le,urlObj:pe,urlToSanitize:de}=this.#i(t,s,a,i);return!k||!this.#a(y,le,c,l,f)?null:d?this.#c(pe,y,i):this.#d(de)}#o({allow:t,deny:e,only:s},n){let o=this.#e,i=!1,a=new Map(n.schemeMap);if(Array.isArray(s)&&s.length){o=new Set,i=!0;for(let c of s)p(c)&&this.#r(c,"only",o,a,n)}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,n)}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:i,schemeMap:a}}#r(t,e,s,n,o){let i=this.normalize(t,!0);return this.#n(i)?(n.set(i,!0),s.add(i),!0):!1}#n(t){return v.test(t)?!1:!t.split("+").some(n=>x.test(n))&&j.test(t)}#i(t,e,s,n){let o=this.parse(t),i=this.verifyParsed(o,s),a=!1,c="";if(!i&&e&&!Q.test(t)){let f=this.parse(t,ne);f&&f.protocol==="http:"&&f.hostname==="dummy.local"&&(i=!0,a=!0,c=`${f.pathname}${f.search}${f.hash}`)}if(!i)return{isVerified:!1};if(a)return{isRelative:a,isVerified:i,isDataUrl:!1,scheme:"",schemeParts:[],urlToSanitize:c};let l=o.protocol.replace(/:$/,""),d=l.split("+");return{isRelative:a,isVerified:i,scheme:l,schemeParts:d,urlObj:o,isDataUrl:l==="data",urlToSanitize:o.href}}#a(t,e,s,n,o){if(o)return!0;if(s)return n.has(t)||e.every(i=>n.get(i));for(let[i,a]of n.entries())if(!(a||t!==i&&e.every(l=>l!==i)))return!1;return!0}#c(t,e,s){let{mediaType:n,mediaTypes:o,data:i,isBase64:a}=A(t.pathname,t.search,t.hash),c=i;if(a)try{c=se(i)}catch(l){return s.debug&&T("Failed to parse base64 data.",l),null}try{let l=te(c).trim(),d=this.parse(l,ne);if(!d)return null;let f=d.protocol;if($(f).some(y=>v.test(y)))return null}catch(l){return s.debug&&T("Failed to parse inner data URL protocol.",l),null}return(!n||G.test(n))&&(c=this.#l(c,s)),c?(a&&c!==i&&o.pop(),`${e}:${o.join(";")},${c}`):null}#l(t,e){let s=t;try{s=decodeURIComponent(t)}catch{}let n,o=(i,a)=>this.#p(i,a,e);e.domPurify.addHook("uponSanitizeAttribute",o);try{n=e.domPurify.sanitize(s)}finally{e.domPurify.removeHook("uponSanitizeAttribute")}n=ee(n);try{return encodeURI(n)}catch{return n}}#p(t,e,s){if(!e.attrValue||!/^\s*data:/i.test(e.attrValue))return;let n=this.parse(e.attrValue);if(!n||n.protocol!=="data:")return;let o=e.attrValue;if(!s.enter(o)){e.attrValue="";return}try{let i=this.#s(o,{allow:["data"],deny:[],only:[],allowRelative:!1},s);e.attrValue=i||""}finally{s.leave(o)}}#d(t){let e=t,s=[U,q];for(let n of s){let o=n.exec(e);o&&(e=e.substring(0,o.index).replace(/[?&]$/,""))}return e}sanitize(t,e={}){let s={...S,...e};return this.#t(t,s)}inspect(t){let e={input:t,valid:!1};if(!p(t))return e.reason=`Invalid URL input: ${g(t)}`,e;if(t==="")return e.reason="Invalid URL input: (empty string)",e;let s,n=null;try{s=this.#t(t,{...S,allow:["data"],allowRelative:!0}),s||(n="Sanitization failed (blocked by allowed schemes or rules).")}catch(o){n=o.message}if(s){let o=this.parse(s);if(e.valid=!0,o){if($(o.protocol).includes("data")){let{mediaTypes:c,data:l,isBase64:d}=A(o.pathname,o.search,o.hash);d&&c.pop(),e.data={mime:c.join(";"),base64:d,data:l}}else e.data=null;for(let c of Ne){let l=o[c];p(l)&&(e[c]=l)}}else e.valid=!1,e.relative=!0,e.href=s,e.reason="Invalid as an absolute URL, but valid as a relative URL."}else e.valid=!1,e.reason=n;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 ${u(t)}.`);let e=this.normalize(t,!0);if(!this.#n(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)}},$e=async r=>{let t=r.type?`${r.type};base64`:"base64",e=await r.arrayBuffer(),s=globalThis.Buffer.from(e).toString("base64");return`data:${t},${s}`},Ae=r=>new Promise((t,e)=>{let s=new globalThis.FileReader;s.addEventListener("error",()=>{let n=s.error||new DOMException("Failed to read Blob via FileReader.","NotReadableError");e(n)}),s.addEventListener("abort",()=>t(null)),s.addEventListener("load",()=>t(s.result)),s.readAsDataURL(r)}),Oe=async r=>{let t=r.type?`${r.type};base64`:"base64",e=await r.arrayBuffer(),s=new Uint8Array(e),n=[];for(let o=0;o<s.length;o+=8192)n.push(String.fromCharCode(...s.subarray(o,o+8192)));return`data:${t},${btoa(n.join(""))}`},ie=async(r,t,e,s=0)=>{let{done:n,value:o}=await r.read();if(n)return;let i=s+o.byteLength;if(i>t){await r.cancel("Size limit exceeded");let a=`Payload (${i} bytes) exceeds max (${t} bytes).`;throw new DOMException(a,"NotReadableError")}return e.push(o),ie(r,t,e,i)},ae=async(r,t)=>{let e=16777216;Number.isInteger(t)&&t>0&&(e=t);let s=await fetch(r);if(!s.ok){let a=`Failed to fetch ${g(r)}`;throw Number.isInteger(s.status)&&(s.statusText?a+=`: ${s.status} ${s.statusText}`:a+=`: ${s.status}`),new Error(a)}let n=s.headers.get("content-length");if(n){let i=Number.parseInt(n,10);if(Number.isInteger(i)&&i>e){let a=`Payload (${i} bytes) exceeds max (${e} bytes).`;throw new DOMException(a,"NotReadableError")}}let o;if(s.body){let i=s.body.getReader(),a=[];try{await ie(i,e,a,0)}finally{i.releaseLock()}let c=s.headers.get("content-type");c?o=new Blob(a,{type:c}):o=new Blob(a)}else if(o=await s.blob(),o.size>e){let i=`Payload (${o.size} bytes) exceeds max (${e} bytes).`;throw new DOMException(i,"NotReadableError")}return Le&&globalThis.Buffer?$e(o):globalThis.FileReader?Ae(o):Oe(o)},m=new P,Pe=async(r,t={})=>{if(!r||!p(r))return null;let e={...S,...t},s=m.getScheme(r);if(s===null)return e.allowRelative?m.sanitize(r,e):null;if(s==="blob"){let{allow:n,deny:o,only:i}=e,a=null;if(n.includes("blob")&&!o.includes("blob")||i.includes("blob")){let c;try{c=await ae(r,e.maxBlobSize)}catch(l){e.debug&&T(`Failed to fetch and convert blob URL: ${g(r)}`,l)}c&&(i.length?i.includes("data")||(e.only=[...i,"data"]):(n.includes("data")||(e.allow=[...n,"data"]),e.deny=e.deny.filter(l=>l!=="data")),a=m.sanitize(c,e))}return e.revokeObjectURL&&URL.revokeObjectURL(r),a}return m.sanitize(r,e)},ke=(r,t={})=>{if(!r||!p(r))return null;let e={...S,...t},s=m.getScheme(r);return s===null?e.allowRelative?m.sanitize(r,e):null:s==="blob"?(e.revokeObjectURL&&URL.revokeObjectURL(r),null):m.sanitize(r,e)},Ie=async r=>{if(p(r)){let t=m.parse(r);if(t?.protocol==="blob:")try{let e=await ae(t.href),s=m.inspect(e);return s.input=r,s}catch(e){return{input:r,valid:!1,reason:e.message}}}return m.inspect(r)},ce=r=>m.verify(r),Me=r=>ce(r),De=m;export{De as default,Ie as inspectURL,Me as isURISync,ce as isValidURI,Pe as sanitizeURL,ke as sanitizeURLSync};
|
|
2
2
|
/*!
|
|
3
3
|
* URL Sanitizer
|
|
4
4
|
*
|