url-sanitizer 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -34
- 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 +14 -12
- package/src/mjs/file-reader.js +22 -12
- package/src/mjs/sanitizer.js +47 -62
- package/src/mjs/uri-util.js +5 -13
- package/src/mjs/lib-util.js +0 -37
- package/types/mjs/lib-util.d.ts +0 -3
package/package.json
CHANGED
|
@@ -25,29 +25,28 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"dompurify": "^3.4.
|
|
28
|
+
"dompurify": "^3.4.5",
|
|
29
29
|
"jsdom": "^29.1.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@braintree/sanitize-url": "^7.1.2",
|
|
33
33
|
"@types/jsdom": "^28.0.3",
|
|
34
|
-
"@types/node": "^25.
|
|
34
|
+
"@types/node": "^25.9.1",
|
|
35
35
|
"c8": "^11.0.0",
|
|
36
36
|
"chai": "^6.2.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"copyfiles-ng": "^2.4.7",
|
|
39
39
|
"esbuild": "^0.28.0",
|
|
40
40
|
"eslint": "^9.39.4",
|
|
41
|
-
"eslint-plugin-jsdoc": "^
|
|
41
|
+
"eslint-plugin-jsdoc": "^63.0.0",
|
|
42
42
|
"eslint-plugin-regexp": "^3.1.0",
|
|
43
43
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
44
44
|
"fast-check": "^4.8.0",
|
|
45
45
|
"globals": "^17.6.0",
|
|
46
46
|
"http-server": "^14.1.1",
|
|
47
47
|
"mitata": "^1.0.34",
|
|
48
|
-
"mocha": "^11.7.
|
|
48
|
+
"mocha": "^11.7.6",
|
|
49
49
|
"neostandard": "^0.13.0",
|
|
50
|
-
"npm-run-all2": "^8.0.4",
|
|
51
50
|
"papaparse": "^5.5.3",
|
|
52
51
|
"sinon": "^22.0.0",
|
|
53
52
|
"strict-url-sanitise": "^0.0.1",
|
|
@@ -69,20 +68,23 @@
|
|
|
69
68
|
"scripts": {
|
|
70
69
|
"bench": "node benchmark/benchmark.js",
|
|
71
70
|
"build": "npm run include && npm run tsc && npm run lint && npm test && npm run copy && npm run bundle",
|
|
72
|
-
"bundle": "npm
|
|
71
|
+
"bundle": "npm run bundle:min && npm run bundle:min-wo-dompurify",
|
|
73
72
|
"bundle:min": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer.min.js --bundle --minify --sourcemap bundle/index.js",
|
|
74
73
|
"bundle:min-wo-dompurify": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer-wo-dompurify.min.js --bundle --minify --sourcemap bundle_wo_dompurify/index.js",
|
|
75
74
|
"char": "node scripts/index char -i",
|
|
76
|
-
"copy": "npm
|
|
77
|
-
"copy:src": "copyfiles-ng src/**/* bundle --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js
|
|
78
|
-
"copy:src-wo-dompurify": "copyfiles-ng src/**/* bundle_wo_dompurify --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js
|
|
79
|
-
"copy:repl": "copyfiles-ng repl/dompurify.js repl/file-reader.js
|
|
80
|
-
"copy:repl-wo-dompurify": "copyfiles-ng repl/dompurify-global.js repl/file-reader.js
|
|
75
|
+
"copy": "npm run copy:src && npm run copy:src-wo-dompurify && npm run copy:repl && npm run copy:repl-wo-dompurify",
|
|
76
|
+
"copy:src": "copyfiles-ng src/**/* bundle --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js",
|
|
77
|
+
"copy:src-wo-dompurify": "copyfiles-ng src/**/* bundle_wo_dompurify --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js",
|
|
78
|
+
"copy:repl": "copyfiles-ng repl/dompurify.js repl/file-reader.js bundle/mjs --up=1 --verbose",
|
|
79
|
+
"copy:repl-wo-dompurify": "copyfiles-ng repl/dompurify-global.js repl/file-reader.js bundle_wo_dompurify/mjs --up=1 --verbose && node scripts/index rename --oldpath=bundle_wo_dompurify/mjs/dompurify-global.js --newpath=bundle_wo_dompurify/mjs/dompurify.js -i",
|
|
81
80
|
"include": "node scripts/index include --dir=iana -i",
|
|
82
81
|
"lint": "eslint --fix .",
|
|
83
82
|
"test": "c8 --reporter=text mocha --exit test/*.test.js",
|
|
84
83
|
"test:bdd": "http-server ./ -c-1 -o test/bdd/url-sanitizer.test.html",
|
|
85
84
|
"tsc": "node scripts/index clean --dir=types -i && npx tsc"
|
|
86
85
|
},
|
|
87
|
-
"
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=22"
|
|
88
|
+
},
|
|
89
|
+
"version": "3.0.0"
|
|
88
90
|
}
|
package/src/mjs/file-reader.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* shared */
|
|
6
|
+
import textChars from '../lib/file/text-chars.json' with { type: 'json' };
|
|
6
7
|
import { getType, isString } from './common.js';
|
|
7
|
-
import { textChars } from './lib-util.js';
|
|
8
8
|
|
|
9
9
|
/* constants */
|
|
10
10
|
import {
|
|
@@ -103,19 +103,17 @@ export class FileReader extends EventTarget {
|
|
|
103
103
|
* @returns {boolean} - result
|
|
104
104
|
*/
|
|
105
105
|
_dispatchProgressEvent(type) {
|
|
106
|
-
if (isString(type)) {
|
|
107
|
-
type = type.trim();
|
|
108
|
-
if (!/abort|error|load(?:end|start)?|progress/.test(type)) {
|
|
109
|
-
this.#error = new DOMException('Invalid state.', 'InvalidStateError');
|
|
110
|
-
this._dispatchProgressEvent('error');
|
|
111
|
-
throw this.#error;
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
106
|
+
if (!isString(type)) {
|
|
114
107
|
this.#error = new TypeError(`Expected String but got ${getType(type)}.`);
|
|
115
108
|
this._dispatchProgressEvent('error');
|
|
116
109
|
throw this.#error;
|
|
117
110
|
}
|
|
118
|
-
|
|
111
|
+
type = type.trim();
|
|
112
|
+
if (!/abort|error|load(?:end|start)?|progress/.test(type)) {
|
|
113
|
+
this.#error = new DOMException('Invalid state.', 'InvalidStateError');
|
|
114
|
+
this._dispatchProgressEvent('error');
|
|
115
|
+
throw this.#error;
|
|
116
|
+
} else if (type === 'error' && !this.#error) {
|
|
119
117
|
this.#error = new Error('Unknown error.');
|
|
120
118
|
}
|
|
121
119
|
const evt = new ProgressEvent(type, {
|
|
@@ -132,9 +130,15 @@ export class FileReader extends EventTarget {
|
|
|
132
130
|
* @returns {void}
|
|
133
131
|
*/
|
|
134
132
|
abort() {
|
|
133
|
+
if (this.#state === this.EMPTY || this.#state === this.DONE) {
|
|
134
|
+
this.#result = null;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
135
137
|
this.#state = this.DONE;
|
|
136
138
|
this.#result = null;
|
|
137
139
|
this.#terminate = true;
|
|
140
|
+
this.#error =
|
|
141
|
+
new DOMException('The read operation was aborted.', 'AbortError');
|
|
138
142
|
this._dispatchProgressEvent('abort');
|
|
139
143
|
this._dispatchProgressEvent('loadend');
|
|
140
144
|
}
|
|
@@ -147,8 +151,14 @@ export class FileReader extends EventTarget {
|
|
|
147
151
|
* @returns {Promise.<void>} - void
|
|
148
152
|
*/
|
|
149
153
|
async _read(blob, format, encoding = '') {
|
|
150
|
-
if (!(blob instanceof Blob
|
|
151
|
-
|
|
154
|
+
if (!(blob instanceof Blob)) {
|
|
155
|
+
throw new TypeError(`Expected Blob but got ${getType(blob)}.`);
|
|
156
|
+
}
|
|
157
|
+
if (!isString(format)) {
|
|
158
|
+
throw new TypeError(`Expected String but got ${getType(format)}.`);
|
|
159
|
+
}
|
|
160
|
+
if (!isString(encoding)) {
|
|
161
|
+
throw new TypeError(`Expected String but got ${getType(encoding)}.`);
|
|
152
162
|
}
|
|
153
163
|
if (!this.#terminate) {
|
|
154
164
|
if (this.#state === this.LOADING) {
|
package/src/mjs/sanitizer.js
CHANGED
|
@@ -64,6 +64,35 @@ class URLSanitizer extends URISchemes {
|
|
|
64
64
|
this.#nest = 0;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* helper to register schemes for allow/only options
|
|
69
|
+
* @private
|
|
70
|
+
* @param {string} item - scheme to register
|
|
71
|
+
* @param {string} listName - name of the option list
|
|
72
|
+
* @param {object} ctx - local context for state management
|
|
73
|
+
* @returns {boolean} - true if scheme is acceptable
|
|
74
|
+
*/
|
|
75
|
+
#registerScheme(item, listName, ctx) {
|
|
76
|
+
if (!REG_SCRIPT_BLOB.test(item)) {
|
|
77
|
+
if (super.has(item)) {
|
|
78
|
+
ctx.schemeMap.set(item, true);
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
super.add(item);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
const msg = `Failed to add scheme '${item}' in '${listName}' list.`;
|
|
84
|
+
logDebug(ctx.debug, msg, e);
|
|
85
|
+
}
|
|
86
|
+
if (super.has(item)) {
|
|
87
|
+
ctx.schemeMap.set(item, true);
|
|
88
|
+
ctx.tempScheme.add(item);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
/**
|
|
68
97
|
* replace matched data URLs
|
|
69
98
|
* @param {string} data - data URL
|
|
@@ -84,9 +113,15 @@ class URLSanitizer extends URISchemes {
|
|
|
84
113
|
}
|
|
85
114
|
this.#nest++;
|
|
86
115
|
this.#recurse.add(dataUrl);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
let parsedDataUrl;
|
|
117
|
+
try {
|
|
118
|
+
parsedDataUrl = this.sanitize(dataUrl, {
|
|
119
|
+
allow: ['data']
|
|
120
|
+
});
|
|
121
|
+
} finally {
|
|
122
|
+
this.#nest--;
|
|
123
|
+
this.#recurse.delete(dataUrl);
|
|
124
|
+
}
|
|
90
125
|
const { index } = item;
|
|
91
126
|
const [preDataUrl, postDataUrl] = [
|
|
92
127
|
replacedData.substring(0, index),
|
|
@@ -149,7 +184,6 @@ class URLSanitizer extends URISchemes {
|
|
|
149
184
|
*/
|
|
150
185
|
sanitize(url, opt) {
|
|
151
186
|
if (this.#nest > HEX) {
|
|
152
|
-
this.#nest = 0;
|
|
153
187
|
throw new Error('Data URLs nested too deeply.');
|
|
154
188
|
}
|
|
155
189
|
const {
|
|
@@ -159,69 +193,36 @@ class URLSanitizer extends URISchemes {
|
|
|
159
193
|
['blob', false],
|
|
160
194
|
['data', false],
|
|
161
195
|
['file', false],
|
|
162
|
-
['
|
|
196
|
+
['javascript', false],
|
|
163
197
|
['vbscript', false]
|
|
164
198
|
]);
|
|
165
199
|
const tempScheme = new Set();
|
|
200
|
+
const ctx = { schemeMap, tempScheme, debug };
|
|
166
201
|
let restrictScheme = false;
|
|
167
202
|
if (Array.isArray(only) && only.length) {
|
|
168
203
|
const schemes = super.get();
|
|
169
204
|
for (const item of schemes) {
|
|
170
205
|
schemeMap.set(item, false);
|
|
171
206
|
}
|
|
172
|
-
|
|
173
|
-
for (let item of items) {
|
|
207
|
+
for (let item of only) {
|
|
174
208
|
if (isString(item)) {
|
|
175
209
|
item = item.trim();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
} else {
|
|
180
|
-
try {
|
|
181
|
-
super.add(item);
|
|
182
|
-
} catch (e) {
|
|
183
|
-
const msg = `Failed to add scheme '${item}' in 'only' list.`;
|
|
184
|
-
logDebug(debug, msg, e);
|
|
185
|
-
}
|
|
186
|
-
if (super.has(item)) {
|
|
187
|
-
schemeMap.set(item, true);
|
|
188
|
-
tempScheme.add(item);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (!restrictScheme && schemeMap.has(item)) {
|
|
192
|
-
restrictScheme = schemeMap.get(item);
|
|
193
|
-
}
|
|
210
|
+
const registered = this.#registerScheme(item, 'only', ctx);
|
|
211
|
+
if (registered && !restrictScheme && schemeMap.has(item)) {
|
|
212
|
+
restrictScheme = schemeMap.get(item);
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
}
|
|
197
216
|
} else {
|
|
198
217
|
if (Array.isArray(allow) && allow.length) {
|
|
199
|
-
const
|
|
200
|
-
for (let item of items) {
|
|
218
|
+
for (const item of allow) {
|
|
201
219
|
if (isString(item)) {
|
|
202
|
-
item
|
|
203
|
-
if (!REG_SCRIPT_BLOB.test(item)) {
|
|
204
|
-
if (super.has(item)) {
|
|
205
|
-
schemeMap.set(item, true);
|
|
206
|
-
} else {
|
|
207
|
-
try {
|
|
208
|
-
super.add(item);
|
|
209
|
-
} catch (e) {
|
|
210
|
-
const msg = `Failed to add scheme '${item}' in 'allow' list.`;
|
|
211
|
-
logDebug(debug, msg, e);
|
|
212
|
-
}
|
|
213
|
-
if (super.has(item)) {
|
|
214
|
-
schemeMap.set(item, true);
|
|
215
|
-
tempScheme.add(item);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
220
|
+
this.#registerScheme(item.trim(), 'allow', ctx);
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
224
|
if (Array.isArray(deny) && deny.length) {
|
|
223
|
-
|
|
224
|
-
for (let item of items) {
|
|
225
|
+
for (let item of deny) {
|
|
225
226
|
if (isString(item)) {
|
|
226
227
|
item = item.trim();
|
|
227
228
|
if (item) {
|
|
@@ -271,7 +272,6 @@ class URLSanitizer extends URISchemes {
|
|
|
271
272
|
}
|
|
272
273
|
if (bool) {
|
|
273
274
|
const isDataUrl = isRelative ? false : schemeParts.includes('data');
|
|
274
|
-
let finalize;
|
|
275
275
|
let urlToSanitize = isRelative ? url : href;
|
|
276
276
|
if (isDataUrl) {
|
|
277
277
|
const [mediaType, ...dataParts] = pathname.split(',');
|
|
@@ -297,15 +297,7 @@ class URLSanitizer extends URISchemes {
|
|
|
297
297
|
if (parsedData !== data || containsDataUrl) {
|
|
298
298
|
if (containsDataUrl) {
|
|
299
299
|
parsedData = this.replace(parsedData);
|
|
300
|
-
} else if (this.#recurse.has(url)) {
|
|
301
|
-
this.#recurse.delete(url);
|
|
302
|
-
} else {
|
|
303
|
-
finalize = true;
|
|
304
300
|
}
|
|
305
|
-
} else if (this.#recurse.has(url)) {
|
|
306
|
-
this.#recurse.delete(url);
|
|
307
|
-
} else {
|
|
308
|
-
finalize = true;
|
|
309
301
|
}
|
|
310
302
|
if (!mediaType || REG_MIME_DOM.test(mediaType)) {
|
|
311
303
|
parsedData = this.purify(parsedData);
|
|
@@ -318,8 +310,6 @@ class URLSanitizer extends URISchemes {
|
|
|
318
310
|
} else {
|
|
319
311
|
urlToSanitize = '';
|
|
320
312
|
}
|
|
321
|
-
} else {
|
|
322
|
-
finalize = true;
|
|
323
313
|
}
|
|
324
314
|
if (!isDataUrl && REG_TAG_QUOT.test(urlToSanitize)) {
|
|
325
315
|
const item = REG_TAG_QUOT.exec(urlToSanitize);
|
|
@@ -330,11 +320,6 @@ class URLSanitizer extends URISchemes {
|
|
|
330
320
|
if (urlToSanitize) {
|
|
331
321
|
sanitizedUrl =
|
|
332
322
|
urlToSanitize.replace(/%26/g, escapeURLEncodedHTMLChars);
|
|
333
|
-
if (finalize) {
|
|
334
|
-
this.#nest = 0;
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
this.#nest = 0;
|
|
338
323
|
}
|
|
339
324
|
}
|
|
340
325
|
if (tempScheme.size) {
|
package/src/mjs/uri-util.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* shared */
|
|
6
|
+
import textChars from '../lib/file/text-chars.json' with { type: 'json' };
|
|
7
|
+
import uriSchemes from '../lib/iana/uri-schemes.json' with { type: 'json' };
|
|
6
8
|
import { getType, isString } from './common.js';
|
|
7
9
|
import { FileReader } from './file-reader.js';
|
|
8
|
-
import { textChars, uriSchemes } from './lib-util.js';
|
|
9
10
|
|
|
11
|
+
/* constants */
|
|
10
12
|
import {
|
|
11
13
|
HEX, MAX_BLOB_SIZE, REG_B64, REG_NUM_REF, REG_SCHEME, REG_SCHEME_EXT,
|
|
12
14
|
REG_SCRIPT, REG_URL_ENC
|
|
@@ -154,14 +156,7 @@ export const createDataURLFromBlob = (blob, maxBlobSize = MAX_BLOB_SIZE) =>
|
|
|
154
156
|
*/
|
|
155
157
|
export class URISchemes {
|
|
156
158
|
/* private fields */
|
|
157
|
-
#schemes;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* construct
|
|
161
|
-
*/
|
|
162
|
-
constructor() {
|
|
163
|
-
this.#schemes = new Set(uriSchemes);
|
|
164
|
-
}
|
|
159
|
+
#schemes = new Set(uriSchemes);
|
|
165
160
|
|
|
166
161
|
/**
|
|
167
162
|
* get schemes
|
|
@@ -234,9 +229,6 @@ export class URISchemes {
|
|
|
234
229
|
* @returns {void}
|
|
235
230
|
*/
|
|
236
231
|
reset() {
|
|
237
|
-
this.#schemes
|
|
238
|
-
for (const scheme of uriSchemes) {
|
|
239
|
-
this.#schemes.add(scheme);
|
|
240
|
-
}
|
|
232
|
+
this.#schemes = new Set(uriSchemes);
|
|
241
233
|
}
|
|
242
234
|
}
|
package/src/mjs/lib-util.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib-util.js
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
8
|
-
/* constants */
|
|
9
|
-
const CHAR = 'utf8';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* parse json
|
|
13
|
-
* @param {string} url - file url
|
|
14
|
-
* @returns {string|Array|object} - parsed json
|
|
15
|
-
*/
|
|
16
|
-
export const parseJson = url => {
|
|
17
|
-
const { href } = new URL(url, import.meta.url);
|
|
18
|
-
const filePath = fileURLToPath(href);
|
|
19
|
-
const file = fs.readFileSync(filePath, {
|
|
20
|
-
encoding: CHAR,
|
|
21
|
-
flag: 'r'
|
|
22
|
-
});
|
|
23
|
-
const content = JSON.parse(file);
|
|
24
|
-
return content;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* text chars
|
|
29
|
-
* @type {Array.<number>}
|
|
30
|
-
*/
|
|
31
|
-
export const textChars = parseJson('../lib/file/text-chars.json');
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* uri schemes
|
|
35
|
-
* @type {Array.<string>}
|
|
36
|
-
*/
|
|
37
|
-
export const uriSchemes = parseJson('../lib/iana/uri-schemes.json');
|
package/types/mjs/lib-util.d.ts
DELETED