url-sanitizer 3.0.0 → 4.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 +117 -53
- 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 +9 -6
- package/src/index.js +2 -2
- package/src/lib/file/text-chars.json +1 -1
- package/src/mjs/common.js +7 -6
- package/src/mjs/constant.js +39 -19
- package/src/mjs/dompurify.js +6 -0
- package/src/mjs/file-reader.js +173 -127
- package/src/mjs/regexp.js +78 -0
- package/src/mjs/sanitizer.js +451 -239
- package/src/mjs/text-chars.js +68 -0
- package/src/mjs/uri-util.js +174 -162
- package/types/index.d.ts +1 -1
- package/types/mjs/common.d.ts +2 -2
- package/types/mjs/constant.d.ts +5 -17
- package/types/mjs/dompurify.d.ts +1 -1
- package/types/mjs/file-reader.d.ts +10 -10
- package/types/mjs/regexp.d.ts +12 -0
- package/types/mjs/sanitizer.d.ts +30 -15
- package/types/mjs/text-chars.d.ts +3 -0
- package/types/mjs/uri-util.d.ts +9 -10
package/README.md
CHANGED
|
@@ -7,31 +7,34 @@
|
|
|
7
7
|
|
|
8
8
|
A robust URL sanitizer for Node.js, browsers, and websites.
|
|
9
9
|
It sanitizes not only regular URLs but also deeply inspects `data` URLs and `blob` URLs.
|
|
10
|
-
It also provides built-in utilities to
|
|
11
|
-
|
|
12
|
-
## Features
|
|
13
|
-
|
|
14
|
-
* **Secure by Default**: Automatically denies `javascript:` and `vbscript:` schemes.
|
|
15
|
-
* **Deep Data URL Inspection**: Parses, decodes (including base64), and sanitizes nested data URLs.
|
|
16
|
-
* **Blob URL Support**: Converts and sanitizes blob URLs into safe data URLs (async).
|
|
17
|
-
* **Relative & Absolute Path Support**: Safely allows same-origin absolute paths and relative paths via an opt-in parameter.
|
|
18
|
-
* **DOMPurify Integration**: Purifies HTML/SVG content embedded within data URLs. **Note:** This library is optimized for URL sanitization and is **not intended** as a complete HTML sanitizer for arbitrary untrusted markup.
|
|
19
|
-
* **Pure ESM with TypeScript Support**: Works seamlessly across modern environments like Node.js, Deno, websites, and browsers (including WebExtensions).
|
|
10
|
+
It also provides built-in utilities to inspect URLs and verify URI schemes.
|
|
20
11
|
|
|
21
12
|
## Table of Contents
|
|
13
|
+
* [Features](#features)
|
|
22
14
|
* [Install](#install)
|
|
23
15
|
* [Usage](#usage)
|
|
24
16
|
* [API Reference](#api-reference)
|
|
25
17
|
* [sanitizeURL(url, opt)](#sanitizeurlurl-opt)
|
|
26
18
|
* [sanitizeURLSync(url, opt)](#sanitizeurlsyncurl-opt)
|
|
27
|
-
* [
|
|
28
|
-
* [
|
|
19
|
+
* [inspectURL(url)](#inspecturlurl)
|
|
20
|
+
* [inspectURLSync(url)](#inspecturlsyncurl)
|
|
29
21
|
* [isURI(uri)](#isuriuri)
|
|
30
22
|
* [isURISync(uri)](#isurisyncuri)
|
|
31
23
|
* [urlSanitizer Instance](#urlsanitizer)
|
|
24
|
+
* [Threat Model](#threat-model)
|
|
25
|
+
* [AI / LLM Application Security](#ai--llm-application-security)
|
|
32
26
|
* [Performance](#performance)
|
|
33
27
|
* [Acknowledgments](#acknowledgments)
|
|
34
28
|
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
* **Secure by Default**: Strictly blocks `javascript:` and `vbscript:` schemes.
|
|
32
|
+
* **Deep Data URL Inspection**: Parses, decodes (including base64), and sanitizes nested data URLs.
|
|
33
|
+
* **Blob URL Support**: Converts and sanitizes blob URLs into safe data URLs (async).
|
|
34
|
+
* **Relative & Absolute Path Support**: Safely allows root-relative paths (e.g., `/foo`) and relative paths (e.g., `./foo`) via an opt-in parameter.
|
|
35
|
+
* **DOMPurify Integration**: Purifies HTML/SVG content embedded within data URLs.
|
|
36
|
+
* **Pure ESM with TypeScript Support**: Works seamlessly across modern environments like Node.js, Deno, websites, and browsers (including WebExtensions).
|
|
37
|
+
|
|
35
38
|
## Install
|
|
36
39
|
|
|
37
40
|
### Node.js
|
|
@@ -42,12 +45,13 @@ npm i url-sanitizer
|
|
|
42
45
|
|
|
43
46
|
### Deno
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
``` javascript
|
|
48
|
+
```javascript
|
|
48
49
|
import urlSanitizer from 'npm:url-sanitizer';
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
**Note:** This library assumes a Node.js/npm-compatible dependency resolution environment, so we recommend using the `npm:` specifier.
|
|
53
|
+
CDN-based ESM imports (e.g., `esm.sh`, `jsdelivr`, or `unpkg`) may work, but are not tested or guaranteed.
|
|
54
|
+
|
|
51
55
|
### Browsers and Websites
|
|
52
56
|
|
|
53
57
|
Standalone ESM builds are available in the `dist/` directory:
|
|
@@ -64,12 +68,18 @@ Alternatively, download them from [Releases](https://github.com/asamuzaK/urlSani
|
|
|
64
68
|
|
|
65
69
|
**Note:** `url-sanitizer-wo-dompurify.min.js` is a lightweight build without [DOMPurify](https://www.npmjs.com/package/dompurify) bundled.
|
|
66
70
|
If you use this build, ensure DOMPurify is exposed globally (e.g., `window.DOMPurify`), otherwise the sanitizer will throw an error at runtime.
|
|
71
|
+
``` html
|
|
72
|
+
<script src="https://unpkg.com/dompurify/dist/purify.min.js"></script>
|
|
73
|
+
<script type="module">
|
|
74
|
+
import urlSanitizer from 'path/to/url-sanitizer-wo-dompurify.min.js';
|
|
75
|
+
</script>
|
|
76
|
+
```
|
|
67
77
|
|
|
68
78
|
## Usage
|
|
69
79
|
|
|
70
80
|
``` javascript
|
|
71
81
|
import urlSanitizer, {
|
|
72
|
-
isURI, isURISync,
|
|
82
|
+
isURI, isURISync, inspectURL, inspectURLSync, sanitizeURL, sanitizeURLSync
|
|
73
83
|
} from 'url-sanitizer';
|
|
74
84
|
```
|
|
75
85
|
|
|
@@ -80,19 +90,30 @@ import urlSanitizer, {
|
|
|
80
90
|
Sanitizes the given URL asynchronously.
|
|
81
91
|
|
|
82
92
|
* `blob`, `data`, and `file` schemes must be explicitly allowed.
|
|
83
|
-
*
|
|
93
|
+
* <a name="about-file-scheme">**WARNING**</a><br>
|
|
94
|
+
**File URLs:** Allowing the `file` scheme can be extremely dangerous in web applications, as it may expose the host's local file system to attacks like Local File Inclusion (LFI). Only allow it if you fully trust the input or are operating in a strictly isolated local environment.
|
|
95
|
+
* **Blob URLs:** Given a **blob** URL, it converts and returns a sanitized **data** URL.
|
|
96
|
+
* The sanitized data URL will **not** be converted back to a blob URL.
|
|
97
|
+
* It is highly recommended to set the `opt.revokeObjectURL` option to `true` when you allow `blob` URLs.
|
|
98
|
+
* Converting a blob URL to a data URL consumes memory, so be aware of the risk of memory exhaustion when handling huge blobs.
|
|
99
|
+
* You can restrict the allowed blob size using the `opt.maxBlobSize` option.
|
|
100
|
+
Default is `16_777_216` (16MB).
|
|
101
|
+
While 16MB may be suitable for client-side processing, it is highly recommended to reduce this limit to `1_048_576` (1MB) or `2_097_152` (2MB) in server-side applications.
|
|
102
|
+
Parsing large HTML/SVG contents inside Data/Blob URLs via JSDOM can consume massive amounts of V8 heap memory.
|
|
84
103
|
|
|
85
104
|
#### Parameters
|
|
86
105
|
|
|
87
106
|
* url **string** URL input.
|
|
88
|
-
* opt **
|
|
89
|
-
* opt.allow
|
|
90
|
-
* opt.deny
|
|
91
|
-
* opt.only
|
|
107
|
+
* opt **\[object\]** Options.
|
|
108
|
+
* opt.allow **\[Array<string>\]** Array of allowed schemes, e.g., \['data'\].
|
|
109
|
+
* opt.deny **\[Array<string>\]** Array of denied schemes, e.g., \['web+foo'\].
|
|
110
|
+
* opt.only **\[Array<string>\]** Array of specific schemes to allow, e.g., \['git', 'https'\].
|
|
92
111
|
`only` takes precedence over `allow` and `deny`.
|
|
93
|
-
* opt.allowRelative **
|
|
94
|
-
* opt.debug **
|
|
95
|
-
* opt.
|
|
112
|
+
* opt.allowRelative **\[boolean\]** If `true`, allows root-relative paths (e.g. `/foo`) and relative paths (e.g. `./foo`). Default is `false`.
|
|
113
|
+
* opt.debug **\[boolean\]** If `true`, outputs internal error/warning logs to the console. Default is `false`.
|
|
114
|
+
* 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`.
|
|
115
|
+
* opt.maxBlobSize **\[number\]** Maximum allowed blob size in bytes. Default is `16_777_216` (16MB).
|
|
116
|
+
* opt.maxLength **\[number\]** Maximum allowed URL length. Default is no limit.
|
|
96
117
|
|
|
97
118
|
**Returns** **Promise<string?>** Sanitized URL, nullable.
|
|
98
119
|
|
|
@@ -197,44 +218,49 @@ const res8 = await sanitizeURL('git+https://example.com/foo.git?<script>alert(1)
|
|
|
197
218
|
Synchronous version of `sanitizeURL()`.
|
|
198
219
|
|
|
199
220
|
* `data` and `file` schemes must be explicitly allowed.
|
|
221
|
+
* **File URLs:** Allowing the `file` scheme can be extremely dangerous in web applications. [See above](#about-file-scheme)
|
|
222
|
+
* **Blob URLs:** `blob` scheme is **not supported** and will return `null`.
|
|
223
|
+
Use the async version for `blob`.
|
|
224
|
+
However, to prevent memory leaks, it is highly recommended to set the `opt.revokeObjectURL` option to `true` so that the unsupported blob URL is properly revoked before returning `null`.
|
|
200
225
|
|
|
201
|
-
|
|
202
|
-
Use the async version for `blob`.
|
|
226
|
+
### inspectURL(url)
|
|
203
227
|
|
|
204
|
-
|
|
228
|
+
**Note:** `parseURL()` is deprecated. Please use `inspectURL()` instead.
|
|
205
229
|
|
|
206
|
-
|
|
230
|
+
Inspects, parses, and sanitizes the given URL asynchronously.
|
|
207
231
|
|
|
208
|
-
* **Data URLs
|
|
209
|
-
|
|
210
|
-
|
|
232
|
+
* **Data URLs:** The embedded payload is fully decoded and sanitized (e.g., removing malicious HTML/SVG attributes) before being safely re-encoded.
|
|
233
|
+
* **Blob URLs:** Simply parsed, but **neither decoded nor sanitized** at this stage.
|
|
234
|
+
To process and sanitize the content of a blob URL, use [sanitizeURL()](#sanitizeurlurl-opt).
|
|
211
235
|
|
|
212
236
|
#### Parameters
|
|
213
237
|
|
|
214
238
|
* url **string** URL input.
|
|
215
239
|
|
|
216
|
-
**Returns** **Promise<
|
|
240
|
+
**Returns** **Promise<InspectedURLResult>** Result.
|
|
217
241
|
|
|
218
|
-
####
|
|
242
|
+
#### InspectedURLResult
|
|
219
243
|
|
|
220
|
-
An object
|
|
244
|
+
An object extending the standard [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API with additional properties.
|
|
245
|
+
The properties except for `input` and `valid` are omitted from the object for invalid URLs.
|
|
221
246
|
|
|
222
|
-
* `input` **string** URL input.
|
|
223
|
-
* `valid` **boolean**
|
|
224
|
-
* `
|
|
225
|
-
|
|
226
|
-
* `data.
|
|
227
|
-
* `data.
|
|
228
|
-
* `
|
|
247
|
+
* `input` — **string** The original URL input.
|
|
248
|
+
* `valid` — **boolean** Indicates whether the URI is valid.
|
|
249
|
+
* `reason` — **[string]** The reason why the URL is invalid. Omitted if `valid` is `true`.
|
|
250
|
+
* `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.
|
|
251
|
+
* `data.mime` — **string** The MIME type of the data.
|
|
252
|
+
* `data.base64` — **boolean** Indicates whether the data is base64-encoded.
|
|
253
|
+
* `data.data` — **string** The actual data part of the data URL.
|
|
254
|
+
* `href`, `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).
|
|
229
255
|
|
|
230
256
|
``` javascript
|
|
231
|
-
const res1 = await
|
|
257
|
+
const res1 = await inspectURL('javascript:alert(1)');
|
|
232
258
|
/* => {
|
|
233
259
|
input: 'javascript:alert(1)',
|
|
234
260
|
valid: false
|
|
235
261
|
} */
|
|
236
262
|
|
|
237
|
-
const res2 = await
|
|
263
|
+
const res2 = await inspectURL('https://www.example.com/?foo=bar#baz');
|
|
238
264
|
/* => {
|
|
239
265
|
input: 'https://www.example.com/?foo=bar#baz',
|
|
240
266
|
valid: true,
|
|
@@ -250,7 +276,7 @@ const res2 = await parseURL('https://www.example.com/?foo=bar#baz');
|
|
|
250
276
|
} */
|
|
251
277
|
|
|
252
278
|
// base64 encoded SVG '<svg><g onclick="alert(1)"/></svg>'
|
|
253
|
-
const res3 = await
|
|
279
|
+
const res3 = await inspectURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==');
|
|
254
280
|
/* => {
|
|
255
281
|
input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==',
|
|
256
282
|
valid: true,
|
|
@@ -267,7 +293,7 @@ const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhb
|
|
|
267
293
|
} */
|
|
268
294
|
|
|
269
295
|
// base64 encoded PNG
|
|
270
|
-
const res4 = await
|
|
296
|
+
const res4 = await inspectURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
|
|
271
297
|
/* => {
|
|
272
298
|
input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
|
|
273
299
|
valid: true,
|
|
@@ -288,7 +314,7 @@ const blob5 = new Blob(['<svg><g onload="alert(1)"/></svg>'], {
|
|
|
288
314
|
type: 'image/svg+xml'
|
|
289
315
|
});
|
|
290
316
|
const url5 = URL.createObjectURL(blob5);
|
|
291
|
-
const res5 = await
|
|
317
|
+
const res5 = await inspectURL(url5);
|
|
292
318
|
/* => {
|
|
293
319
|
input: 'blob:nodedata:82ecc5a4-aea8-48d7-a407-64e2ef0913da',
|
|
294
320
|
valid: true,
|
|
@@ -301,13 +327,15 @@ const res5 = await parseURL(url5);
|
|
|
301
327
|
} */
|
|
302
328
|
```
|
|
303
329
|
|
|
304
|
-
###
|
|
330
|
+
### inspectURLSync(url)
|
|
331
|
+
|
|
332
|
+
**Note:** `parseURLSync()` is deprecated. Please use `inspectURLSync()` instead.
|
|
305
333
|
|
|
306
|
-
Synchronous version of `
|
|
334
|
+
Synchronous version of `inspectURL()`.
|
|
307
335
|
|
|
308
336
|
### isURI(uri)
|
|
309
337
|
|
|
310
|
-
Checks if the given string is a valid URI and whether its scheme is
|
|
338
|
+
Checks if the given string is a valid URI and whether its scheme is registered in the allowed list.
|
|
311
339
|
|
|
312
340
|
Specifically, it verifies that the input has a correct URI syntax, is not a denied scheme (like `javascript:`), and meets one of the following criteria:
|
|
313
341
|
* The scheme is registered in the default list (e.g., IANA registered schemes, `https`, `mailto`).
|
|
@@ -373,6 +401,42 @@ Remove a scheme from the list of registered URI schemes.
|
|
|
373
401
|
|
|
374
402
|
Reset registered schemes to the default list.
|
|
375
403
|
|
|
404
|
+
## Threat Model
|
|
405
|
+
|
|
406
|
+
To help you decide if this library fits your security requirements, here is the defined threat model detailing what `url-sanitizer` does and does not protect against.
|
|
407
|
+
|
|
408
|
+
### In-Scope (What we protect against)
|
|
409
|
+
|
|
410
|
+
This library is primarily designed to prevent **Cross-Site Scripting (XSS)** and unauthorized protocol execution via malicious URLs.
|
|
411
|
+
|
|
412
|
+
* **Direct XSS Execution:** Blocks `javascript:` and `vbscript:` schemes outright, even if they are obfuscated with whitespaces or control characters.
|
|
413
|
+
* **Nested XSS in Data/Blob URLs:** Deeply inspects and sanitizes payloads within `data:` and `blob:` URLs. If an attacker tries to hide malicious HTML/SVG within a Base64-encoded data URL, the embedded content is purified using DOMPurify and re-encoded back into a sanitized URL.
|
|
414
|
+
* **Unauthorized Schemes:** Denies unknown or unregistered URI schemes by default, preventing application-specific or OS-level protocol hijacking (unless explicitly allowed).
|
|
415
|
+
|
|
416
|
+
### Out-of-Scope (What we DO NOT protect against)
|
|
417
|
+
|
|
418
|
+
This library validates the *syntax and safe construction* of a URL, but it does not validate the *destination or intent* of the server it points to, nor does it secure a compromised host environment.
|
|
419
|
+
|
|
420
|
+
* **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.
|
|
421
|
+
* **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.
|
|
422
|
+
* **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.
|
|
423
|
+
* **Compromised Host Environment:** We assume the native `URL` API is intact. We do not protect against runtime attacks where the global `URL` object has 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.
|
|
424
|
+
|
|
425
|
+
## AI / LLM Application Security
|
|
426
|
+
|
|
427
|
+
With the rise of AI agents, RAG (Retrieval-Augmented Generation), and LLM-driven applications, developers increasingly render content generated directly by AI.
|
|
428
|
+
Unpredictable AI outputs introduce unique security risks, and `url-sanitizer` is architected to serve as a reliable defense layer for these modern environments.
|
|
429
|
+
|
|
430
|
+
### Defending Against Complex & Obfuscated Payloads
|
|
431
|
+
LLMs can generate highly complex, nested, or Base64-encoded `data:` URLs — either through prompt injection, RAG data poisoning, or simply reproducing obfuscated code from their training data.
|
|
432
|
+
Because `url-sanitizer` performs **Deep Data URL Inspection** — decoding the payload, purifying the inner HTML/SVG tree via DOMPurify, and re-encoding it — it physically neutralizes hidden XSS vectors, ensuring that AI-generated data URLs are safe to render.
|
|
433
|
+
|
|
434
|
+
### Neutralizing Hallucinated Schemes
|
|
435
|
+
LLMs generate URLs based on statistical linguistic patterns rather than factual databases.
|
|
436
|
+
As a result, they frequently hallucinate plausible-looking but non-existent or hazardous URI schemes (e.g., `ai-agent://`, `host-settings:`).
|
|
437
|
+
By operating on a **Secure by Default** whitelist approach, `url-sanitizer` automatically denies any unrecognized or unregistered protocols.
|
|
438
|
+
This strict blocking prevents hallucinated schemes from inadvertently triggering application-specific or OS-level protocol hijacking.
|
|
439
|
+
|
|
376
440
|
## Performance
|
|
377
441
|
|
|
378
442
|
Execution times were measured using [mitata](https://github.com/evanwashere/mitata) on Node.js.
|
|
@@ -380,10 +444,10 @@ Execution times were measured using [mitata](https://github.com/evanwashere/mita
|
|
|
380
444
|
### Benchmark Results
|
|
381
445
|
|
|
382
446
|
| 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) |
|
|
383
|
-
| :--- |
|
|
384
|
-
| **Normal HTTP URL** | ~
|
|
385
|
-
| **XSS URL** | ~
|
|
386
|
-
| **Complex Data URL** | ~
|
|
447
|
+
| :--- | :---: | :---: | :---: |
|
|
448
|
+
| **Normal HTTP URL** | ~0.94 µs/iter | ~4.32 µs/iter | ~4.02 µs/iter |
|
|
449
|
+
| **XSS URL** | ~5.28 µs/iter | ~1.52 µs/iter | ~9.44 µs/iter |
|
|
450
|
+
| **Complex Data URL** | ~169.41 µs/iter | ~2.65 µs/iter | ~9.46 µs/iter |
|
|
387
451
|
|
|
388
452
|
### Characteristics & Trade-offs
|
|
389
453
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var K={sanitize:(...t)=>{if(globalThis?.DOMPurify)return globalThis.DOMPurify.sanitize(...t);let e=["DOMPurify is not available.","Ensure DOMPurify is exposed globally (e.g., window.DOMPurify)."];throw new Error(e.join(" "))}};var V=/^[\w+/=-]+$/;var D=/data:[\w#&+./;=-]*,/,I=/data:[\w#&+./;=^]*base64,[\w+/=-]+/i,J=/data:[\w#&+./;=-]*,[^\x22]+/g,W=/&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/gi,ee=/^(?:application\/(?:[\w#&.;-]+\+)?x|image\/svg\+x|text\/(?:ht|x))ml;?/;var te=/^[a-z][\da-z+.-]*$/,se=/^(?:ext|web)\+[a-z]+$/,O=/(?:java|vb)script/,P=/(?:java|vb)script|blob/,j=/%(?:22|27|3C|3E)|[\x22\x27<>]/,re=/^%[\dA-F]{2}$/i;var b=t=>Object.prototype.toString.call(t).slice(8,-1),m=t=>typeof t=="string"||t instanceof String;var B=[7,8,9,10,11,12,13,27,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,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,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 F=["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","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","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","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","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","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","upn","urn","ut2004","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{FileReader:ie}=globalThis;var ae=t=>{if(!m(t))throw new TypeError(`Expected String but got ${b(t)}.`);let e=[];for(let s of t)e.push(`%${s.charCodeAt(0).toString(16).toUpperCase()}`);return e.join("")},oe=t=>{m(t)&&re.test(t)&&(t=t.toUpperCase());let[e,s,r,i,a,n]=["&","#","<",">",'"',"'"].map(ae),o;return t===e?o=`${e}amp;`:t===r?o=`${e}lt;`:t===i?o=`${e}gt;`:t===a?o=`${e}quot;`:t===n?o=`${e}${s}39;`:o=t,o},ne=t=>{if(m(t)){if(!V.test(t))throw new Error(`Invalid base64 data: ${t}`)}else throw new TypeError(`Expected String but got ${b(t)}.`);let e=atob(t),s=Uint8Array.from([...e].map(a=>a.charCodeAt(0))),r=new Set(B),i;return s.every(a=>r.has(a))?i=e.replace(/\s/g,ae):i=t,i},N=(t,e=0)=>{if(!m(t))throw new TypeError(`Expected String but got ${b(t)}.`);if(Number.isInteger(e)){if(e>16)throw new Error("Character references nested too deeply.")}else throw new TypeError(`Expected Number but got ${b(e)}.`);let s=decodeURIComponent(t);if(/&#/.test(s)){let r=new Set(B),i=[...s.matchAll(W)].reverse();for(let a of i){let[n,o]=a,p;if(/^x[\dA-F]+/i.test(o)?p=parseInt(`0${o}`,16):/^\d+/.test(o)&&(p=parseInt(o)),Number.isInteger(p)){let{index:d}=a,[c,h]=[s.substring(0,d),s.substring(d+n.length)];r.has(p)?(s=`${c}${String.fromCharCode(p)}${h}`,(/#x?$/.test(c)||/^#(?:x(?:00)?[2-7]|\d)/.test(h))&&(s=N(s,++e))):p<256&&(s=`${c}${h}`)}}}return s},ce=(t,e=33554432)=>new Promise((s,r)=>{if(Number.isInteger(t?.size)){if(Number.isInteger(e)&&t.size>e)return r(new DOMException(`Blob size (${t.size} bytes) exceeds the maximum allowed size of ${e} bytes.`,"NotReadableError"))}else return s(null);let i=new ie;i.addEventListener("error",()=>r(i.error)),i.addEventListener("abort",()=>s(i.result)),i.addEventListener("load",()=>s(i.result)),i.readAsDataURL(t)}),C=class{#e=new Set(F);get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(m(e)){if(O.test(e)||!te.test(e))throw new Error(`Invalid scheme: ${e}`)}else throw new TypeError(`Expected String but got ${b(e)}.`);return this.#e.add(e),[...this.#e]}remove(e){return this.#e.delete(e)}verify(e){let s;if(m(e))try{let{protocol:r}=new URL(e),i=r.replace(/:$/,""),a=i.split("+");s=!O.test(i)&&se.test(i)||a.every(n=>this.#e.has(n))}catch{s=!1}return!!s}reset(){this.#e=new Set(F)}};var E=(t,e,s)=>{t&&console.warn(`[URLSanitizer Debug] ${e}`,s?s.message:"")},H=class extends C{#e;#t=new Set;constructor(){super(),this.#e=0}#s(e,s,r){if(!P.test(e)){if(super.has(e))r.schemeMap.set(e,!0);else{try{super.add(e)}catch(i){let a=`Failed to add scheme '${e}' in '${s}' list.`;E(r.debug,a,i)}super.has(e)&&(r.schemeMap.set(e,!0),r.tempScheme.add(e))}return!0}return!1}replace(e){if(!m(e))throw new TypeError(`Expected String but got ${b(e)}.`);let s=e;if(D.test(s)){let i=[...s.matchAll(J)].reverse();for(let a of i){let[n]=a;I.test(n)&&([n]=I.exec(n)),this.#e++,this.#t.add(n);let o;try{o=this.sanitize(n,{allow:["data"]})}finally{this.#e--,this.#t.delete(n)}let{index:p}=a,[d,c]=[s.substring(0,p),s.substring(p+n.length)];o?s=`${d}${o}${c}`:s=`${d}${c}`}}return s}purify(e){if(!m(e))throw new TypeError(`Expected String but got ${b(e)}.`);let s=e;try{s=decodeURIComponent(e)}catch{}let r=K.sanitize(s);r&&D.test(r)&&(r=this.replace(r)),r=r.replace(/(?:#|%23)$/,"").replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/,"");try{return encodeURI(r)}catch{return r}}sanitize(e,s){if(this.#e>16)throw new Error("Data URLs nested too deeply.");let{allow:r,deny:i,only:a,allowRelative:n=!1,debug:o=!1}=s??{},p=new Map([["blob",!1],["data",!1],["file",!1],["javascript",!1],["vbscript",!1]]),d=new Set,c={schemeMap:p,tempScheme:d,debug:o},h=!1;if(Array.isArray(a)&&a.length){let l=super.get();for(let u of l)p.set(u,!1);for(let u of a)m(u)&&(u=u.trim(),this.#s(u,"only",c)&&!h&&p.has(u)&&(h=p.get(u)))}else{if(Array.isArray(r)&&r.length)for(let l of r)m(l)&&this.#s(l.trim(),"allow",c);if(Array.isArray(i)&&i.length)for(let l of i)m(l)&&(l=l.trim(),l&&p.set(l,!1))}let S,v=super.verify(e),y=!1;if(!v&&n)try{let{hostname:l,protocol:u}=new URL(e,"http://dummy.local");u==="http:"&&l==="dummy.local"&&(v=!0,y=!0)}catch(l){E(o,"Failed to parse relative URL.",l)}if(v){let l,u,k,q,X,L="",T=[],U=!1;if(y)U=!0;else if({hash:l,href:u,pathname:k,protocol:q,search:X}=new URL(e),L=q.replace(/:$/,""),T=L.split("+"),h)U=T.every(f=>p.get(f));else for(let[f,x]of p.entries())if(U=x||L!==f&&T.every($=>$!==f),!U)break;if(U){let _=y?!1:T.includes("data"),f=y?e:u;if(_){let[x,...$]=k.split(","),A=`${$.join(",")}${X}${l}`,z=x.split(";"),Y=z[z.length-1]==="base64",g=A;Y&&(g=ne(A));try{let G=N(g).trim(),{protocol:Q}=new URL(G);Q.replace(/:$/,"").split("+").some(pe=>P.test(pe))&&(f="")}catch(G){E(o,"Failed to parse inner data URL protocol.",G)}let Z=D.test(g);(g!==A||Z)&&Z&&(g=this.replace(g)),(!x||ee.test(x))&&(g=this.purify(g)),f&&g?(Y&&g!==A&&z.pop(),f=`${L}:${z.join(";")},${g}`):f=""}if(!_&&j.test(f)){let x=j.exec(f),{index:$}=x;f=f.substring(0,$).replace(/[?&]$/,"")}f&&(S=f.replace(/%26/g,oe))}d.size&&d.forEach(_=>{super.remove(_)})}return S||null}parse(e,s){if(!m(e))throw new TypeError(`Expected String but got ${b(e)}.`);let r=new Map([["input",e]]),i;if(this.verify(e)){let{protocol:a}=new URL(e);a==="blob:"?i=e:i=this.sanitize(e,s??{allow:["data","file"]})}if(i){let a=new URL(i),{pathname:n,protocol:o}=a,d=o.replace(/:$/,"").split("+").includes("data");if(r.set("valid",!0),d){let c=new Map,[h,...S]=n.split(","),v=`${S.join(",")}`,y=h.split(";"),l=y[y.length-1]==="base64";l&&y.pop(),c.set("mime",y.join(";")),c.set("base64",l),c.set("data",v),r.set("data",Object.fromEntries(c))}else r.set("data",null);for(let c in a){let h=a[c];m(h)&&r.set(c,h)}}else r.set("valid",!1);return Object.fromEntries(r)}reset(){super.reset(),this.#e=0,this.#t.clear()}};var w=new H,he=async(t,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,maxBlobSize:33554432})=>{let s=!!e?.debug,r=Number.isInteger(e?.maxBlobSize)&&e.maxBlobSize>0?e.maxBlobSize:33554432,i;if(t&&m(t)){let a;try{let{protocol:n}=new URL(t);a=n.replace(/:$/,"")}catch(n){E(s,`Invalid URL input format: ${t}`,n)}if(a==="blob"){let{allow:n,deny:o,only:p}=e;if(Array.isArray(n)&&n.includes("blob")&&!(Array.isArray(o)&&o.includes("blob"))||Array.isArray(p)&&p.includes("blob")){let d;try{d=await fetch(t).then(c=>c.blob()).then(c=>ce(c,r))}catch(c){let h=`Failed to fetch and convert blob URL: ${t}`;E(s,h,c)}if(d){if(Array.isArray(p))p.includes("data")||p.push("data");else if(Array.isArray(n)&&(n.includes("data")||n.push("data"),Array.isArray(o)&&o.includes("data"))){let c=o.indexOf("data");o.splice(c,1)}i=w.sanitize(d,e)}}URL.revokeObjectURL(t)}else(a||e.allowRelative)&&(i=w.sanitize(t,e))}return i||null},ue=(t,e)=>{let s;if(t&&m(t)){let r;try{let{protocol:i}=new URL(t);r=i.replace(/:$/,"")}catch(i){let a=`Failed to parse URL: ${t}`;E(e?.debug,a,i)}r==="blob"?URL.revokeObjectURL(t):(r||e?.allowRelative)&&(s=w.sanitize(t,e))}return s||null},ge=async t=>w.parse(t),be=t=>w.parse(t),ye=async t=>w.verify(t),we=t=>w.verify(t),xe=w;export{xe as default,ye as isURI,we as isURISync,ge as parseURL,be as parseURLSync,he as sanitizeURL,ue as sanitizeURLSync};
|
|
1
|
+
var ue=Object.freeze(["addHook","removeHook","removeAllHooks","setConfig"]),z=[],P=new Proxy({},{get(s,e){let t=globalThis.DOMPurify||globalThis.window?.DOMPurify;if(t){if(z.length){for(let{prop:a,args:n}of z)typeof t[a]=="function"&&t[a](...n);z.length=0}let r=t[e];return typeof r=="function"?r.bind(t):r}return(...r)=>{if(ue.includes(e)){z.push({prop:e,args:r});return}throw new Error("DOMPurify is not available. Ensure DOMPurify is exposed globally (e.g., window.DOMPurify).")}}});var E=s=>Object.prototype.toString.call(s).slice(8,-1),d=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","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","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","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","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","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","upn","urn","ut2004","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{FileReader:J}=globalThis;var ee=/(?:#|%23)$/,te=/^(?:application\/(?:[\w.-]+\+)?xml|image\/svg\+xml|text\/(?:html|xml))(?:\s*;\s*|$)/i;var se=/(?<!(?:#|%23).*)(?:\?|%3F)$/,H=/^[a-z][\da-z+.-]*$/,re=/^(?:web\+[a-z]+|ext\+[a-z][\da-z.-]*)$/,C=/^(?:java|vb)script$/i,B=/^(?:(?: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,ae=/^%[\dA-F]{2}$/i,ne=/^(?:\/{2,}|\\|[a-z][a-z\d+\-.]*:[^/])/i;var oe=[8,9,10,11,12,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 F=new Set(oe),M=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]]),Re=()=>{let s=new Map;for(let e=0;e<256;e++)!F.has(e)&&!M.has(e)&&s.set(e,`\\x${e.toString(16).padStart(2,"0").toUpperCase()}`);return s},X=Re();var[U,Ee,ve,Ue,Se,Le]=["&","#","<",">",'"',"'"].map(s=>`%${s.charCodeAt(0).toString(16).toUpperCase()}`),_e=new RegExp(`[${[...X.values()].join("")}]`),Te=/&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/gi,D=class{#e=new Set(K);get(){return[...this.#e]}has(e){return this.#e.has(e)}verify(e,t=this.#e){if(!d(e))return!1;try{let{protocol:r}=new URL(e),a=r.replace(/:$/,""),n=a.split("+");return n.some(c=>C.test(c))?!1:re.test(a)||n.every(c=>t.has(c))}catch{return!1}}};var ie=s=>{let e=s;if(d(e))switch(ae.test(e)&&(e=e.toUpperCase()),e){case U:return`${U}amp;`;case ve:return`${U}lt;`;case Ue:return`${U}gt;`;case Se:return`${U}quot;`;case Le:return`${U}${Ee}39;`;default:}return e},ce=s=>{if(!d(s))throw new TypeError(`Expected String but got ${E(s)}.`);let e=s.replace(/\s/g,""),t;try{t=atob(e)}catch{throw new Error(`Invalid base64 data: ${s}`)}let r=Uint8Array.from(t,a=>a.charCodeAt(0));try{let n=new TextDecoder("utf-8",{fatal:!0}).decode(r);return _e.test(n)?e:n}catch{return e}},Ce=(s,e)=>{let t=/x/i.test(e[0])?parseInt(e.substring(1),16):parseInt(e,10);if(Number.isInteger(t)){if(F.has(t))return String.fromCharCode(t);if(M.has(t)){let r=M.get(t);return String.fromCodePoint(r)}else if(X.has(t))return""}return s},le=(s,e=0)=>{if(!d(s))throw new TypeError(`Expected String but got ${E(s)}.`);if(!Number.isInteger(e))throw new TypeError(`Expected Number but got ${E(e)}.`);let t=decodeURIComponent(s);for(;/&#/.test(t);){if(e>16)throw new Error("Character references nested too deeply.");let r=t;if(t=t.replace(Te,Ce),t===r)break;e++}return t},pe=(s,e=16777216)=>new Promise((t,r)=>{if(Number.isInteger(s?.size)){if(Number.isInteger(e)&&s.size>e){let n=`Blob size (${s.size} bytes) exceeds max (${e} bytes).`;return r(new DOMException(n,"NotReadableError"))}}else return t(null);let a=new J;a.addEventListener("error",()=>r(a.error)),a.addEventListener("abort",()=>t(a.result)),a.addEventListener("load",()=>t(a.result)),a.readAsDataURL(s)}),me=s=>d(s)?s.replace(ee,"").replace(se,""):s;var $e=Object.freeze(["href","origin","protocol","username","password","host","hostname","port","pathname","search","hash"]),Ae=Object.freeze({RETURN_DOM:!1,RETURN_DOM_FRAGMENT:!1,RETURN_TRUSTED_TYPE:!1}),x=(s,e,t)=>{s&&console.warn(`[URLSanitizer Debug] ${e}`,t?t.message:"")},Y=class s extends D{#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;try{n=new URL(t.attrValue)}catch{return}if(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}`;x(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{P.setConfig(Ae),P.addHook("uponSanitizeAttribute",s.#n)}constructor(){super(),this.#e=new Set(super.get())}#r(e,t,r,a,n){if(B.test(e))return!1;if(e.split("+").some(l=>C.test(l))||!H.test(e)){let l=`Failed to add scheme '${e}' in '${t}' list.`;return x(n.debug,l),!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=me(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{allow:a,deny:n,only:o,allowRelative:c}=t,l=this.#e,h=!1,p=new Map(r.schemeMap);if(o.length){l=new Set;for(let i of o)d(i)&&(i=i.trim(),this.#r(i,"only",l,p,r)&&!h&&(h=!0))}else{if(a.length){l=new Set(this.#e);for(let i of a)d(i)&&this.#r(i.trim(),"allow",l,p,r)}if(n.length)for(let i of n)d(i)&&(i=i.trim(),i&&p.set(i,!1))}let g,f=super.verify(e,l),u=!1,$="";if(!f&&c&&!ne.test(e))try{let i=new URL(e,"http://dummy.local");i.protocol==="http:"&&i.hostname==="dummy.local"&&(f=!0,u=!0,$=`${i.pathname}${i.search}${i.hash}`)}catch(i){x(r.debug,"Failed to parse relative URL.",i)}if(f){let i,y,S,q,Q,A="",O=[],L=!1;if(u)L=!0;else if({hash:i,href:y,pathname:S,protocol:q,search:Q}=new URL(e),A=q.replace(/:$/,""),O=A.split("+"),h)L=O.every(m=>p.get(m));else for(let[m,R]of p.entries())if(L=R||A!==m&&O.every(_=>_!==m),!L)break;if(L){let j=u?!1:O.includes("data"),m=u?$:y;if(j){let[R,..._]=S.split(","),G=`${_.join(",")}${Q}${i}`,I=R.split(";"),W=I[I.length-1]==="base64",w=G;if(W)try{w=ce(G)}catch(T){x(r.debug,"Failed to parse base64 data.",T),m=""}try{let T=le(w).trim(),{protocol:Z}=new URL(T,"http://dummy.local");Z.replace(/:$/,"").split("+").some(he=>B.test(he))&&(m="")}catch(T){x(r.debug,"Failed to parse inner data URL protocol.",T)}(!R||te.test(R))&&(w=this.#o(w,r)),m&&w?(W&&w!==G&&I.pop(),m=`${A}:${I.join(";")},${w}`):m=""}if(!j&&k.test(m)){let R=k.exec(m),{index:_}=R;m=m.substring(0,_).replace(/[?&]$/,"")}m&&(g=m)}}return g||null}sanitize(e,t){if(!e||!d(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://"))&&!k.test(e)&&!e.includes("data:"))try{let{href:c}=new URL(e);return c.replace(/%26/g,ie)}catch{return 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:P,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(!d(e))throw new TypeError(`Expected String but got ${E(e)}.`);let r=new Map([["input",e]]),a,n=null,o=Number.isInteger(t?.maxLength)&&t.maxLength?t.maxLength:0;if(o&&e.length>o)n=`URL length ${e.length} exceeds maxLength ${o}.`;else if(this.verify(e)){let{protocol:c}=new URL(e);if(c==="blob:")a=e;else try{a=this.sanitize(e,t??{allow:["data","file"]}),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 c=new URL(a),{pathname:l,protocol:h}=c,g=h.replace(/:$/,"").split("+").includes("data");if(r.set("valid",!0),g){let f=new Map,[u,...$]=l.split(","),i=`${$.join(",")}${c.search}${c.hash}`,y=u.split(";"),S=y[y.length-1]==="base64";S&&y.pop(),f.set("mime",y.join(";")),f.set("base64",S),f.set("data",i),r.set("data",Object.fromEntries(f))}else r.set("data",null);for(let f of $e){let u=c[f];d(u)&&r.set(f,u)}}else r.set("valid",!1),n&&r.set("reason",n);return Object.fromEntries(r)}get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(!d(e))throw new TypeError(`Expected String but got ${E(e)}.`);if(e.split("+").some(a=>C.test(a))||!H.test(e))throw new Error(`Invalid scheme: ${e}`);return this.#e.add(e),[...this.#e]}remove(e){return this.#e.delete(e)}reset(){this.#e=new Set(super.get())}verify(e,t=this.#e){return super.verify(e,t)}};var b=new Y,Oe=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&&d(s)){let n;try{let{protocol:o}=new URL(s);n=o.replace(/:$/,"")}catch(o){x(t,`Invalid URL input format: ${s}`,o)}if(n==="blob"){let{allow:o,deny:c,only:l}=e;if(Array.isArray(o)&&o.includes("blob")&&!(Array.isArray(c)&&c.includes("blob"))||Array.isArray(l)&&l.includes("blob")){let h;try{h=await fetch(s).then(p=>p.blob()).then(p=>pe(p,r))}catch(p){let g=`Failed to fetch and convert blob URL: ${s}`;x(t,g,p)}if(h){let p={...e};Array.isArray(e.only)?p.only=e.only.includes("data")?e.only:[...e.only,"data"]:Array.isArray(e.allow)&&(p.allow=e.allow.includes("data")?e.allow:[...e.allow,"data"],Array.isArray(e.deny)&&(p.deny=e.deny.filter(g=>g!=="data"))),a=b.sanitize(h,p)}}e?.revokeObjectURL&&URL.revokeObjectURL(s)}else(n||e.allowRelative)&&(a=b.sanitize(s,e))}return a||null},Ie=(s,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,revokeObjectURL:!1})=>{let t=!!e?.debug,r;if(s&&d(s)){let a;try{let{protocol:n}=new URL(s);a=n.replace(/:$/,"")}catch(n){let o=`Invalid URL input format: ${s}`;x(t,o,n)}if(a==="blob")return e?.revokeObjectURL&&URL.revokeObjectURL(s),null;(a||e.allowRelative)&&(r=b.sanitize(s,e))}return r||null},de=async s=>b.inspect(s),ze=s=>de(s),fe=s=>b.inspect(s),Pe=s=>fe(s),ke=async s=>b.verify(s),Me=s=>b.verify(s),De=b;export{De as default,de as inspectURL,fe as inspectURLSync,ke as isURI,Me as isURISync,ze as parseURL,Pe as parseURLSync,Oe as sanitizeURL,Ie as sanitizeURLSync};
|
|
2
2
|
/*!
|
|
3
3
|
* URL Sanitizer
|
|
4
4
|
*
|