url-sanitizer 2.1.0 → 2.3.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 +27 -1
- 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 +3 -3
- package/dist/url-sanitizer.min.js.map +3 -3
- package/package.json +6 -1
- package/src/lib/iana/uri-schemes.json +1 -1
- package/src/mjs/constant.js +2 -0
- package/src/mjs/file-reader.js +8 -2
- package/src/mjs/sanitizer.js +93 -29
- package/src/mjs/uri-util.js +19 -8
- package/types/mjs/constant.d.ts +2 -0
- package/types/mjs/sanitizer.d.ts +3 -0
- package/types/mjs/uri-util.d.ts +1 -1
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"jsdom": "^29.1.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
+
"@braintree/sanitize-url": "^7.1.2",
|
|
32
33
|
"@types/jsdom": "^28.0.3",
|
|
33
34
|
"@types/node": "^25.8.0",
|
|
34
35
|
"c8": "^11.0.0",
|
|
@@ -40,13 +41,16 @@
|
|
|
40
41
|
"eslint-plugin-jsdoc": "^62.9.0",
|
|
41
42
|
"eslint-plugin-regexp": "^3.1.0",
|
|
42
43
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
44
|
+
"fast-check": "^4.8.0",
|
|
43
45
|
"globals": "^17.6.0",
|
|
44
46
|
"http-server": "^14.1.1",
|
|
47
|
+
"mitata": "^1.0.34",
|
|
45
48
|
"mocha": "^11.7.5",
|
|
46
49
|
"neostandard": "^0.13.0",
|
|
47
50
|
"npm-run-all2": "^8.0.4",
|
|
48
51
|
"papaparse": "^5.5.3",
|
|
49
52
|
"sinon": "^22.0.0",
|
|
53
|
+
"strict-url-sanitise": "^0.0.1",
|
|
50
54
|
"typescript": "^6.0.3",
|
|
51
55
|
"undici": "^8.0.2"
|
|
52
56
|
},
|
|
@@ -63,6 +67,7 @@
|
|
|
63
67
|
"serialize-javascript": "^7.0.4"
|
|
64
68
|
},
|
|
65
69
|
"scripts": {
|
|
70
|
+
"bench": "node benchmark/benchmark.js",
|
|
66
71
|
"build": "npm run include && npm run tsc && npm run lint && npm test && npm run copy && npm run bundle",
|
|
67
72
|
"bundle": "npm-run-all -s bundle:*",
|
|
68
73
|
"bundle:min": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer.min.js --bundle --minify --sourcemap bundle/index.js",
|
|
@@ -79,5 +84,5 @@
|
|
|
79
84
|
"test:bdd": "http-server ./ -c-1 -o test/bdd/url-sanitizer.test.html",
|
|
80
85
|
"tsc": "node scripts/index clean --dir=types -i && npx tsc"
|
|
81
86
|
},
|
|
82
|
-
"version": "2.
|
|
87
|
+
"version": "2.3.0"
|
|
83
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
["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"]
|
|
1
|
+
["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"]
|
package/src/mjs/constant.js
CHANGED
package/src/mjs/file-reader.js
CHANGED
|
@@ -7,7 +7,9 @@ import { getType, isString } from './common.js';
|
|
|
7
7
|
import { textChars } from './lib-util.js';
|
|
8
8
|
|
|
9
9
|
/* constants */
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CHUNK_SIZE, REG_CHARSET, REG_MIME_DOM, REG_MIME_TEXT
|
|
12
|
+
} from './constant.js';
|
|
11
13
|
const DONE = 2;
|
|
12
14
|
const EMPTY = 0;
|
|
13
15
|
const LOADING = 1;
|
|
@@ -163,7 +165,11 @@ export class FileReader extends EventTarget {
|
|
|
163
165
|
const mediaTypes = type ? type.split(';') : [];
|
|
164
166
|
const buffer = await blob.arrayBuffer();
|
|
165
167
|
const uint8arr = new Uint8Array(buffer);
|
|
166
|
-
const
|
|
168
|
+
const chunkSize = CHUNK_SIZE;
|
|
169
|
+
let binary = '';
|
|
170
|
+
for (let i = 0; i < uint8arr.length; i += chunkSize) {
|
|
171
|
+
binary += String.fromCharCode(...uint8arr.subarray(i, i + chunkSize));
|
|
172
|
+
}
|
|
167
173
|
this._dispatchProgressEvent('loadstart');
|
|
168
174
|
switch (format) {
|
|
169
175
|
case 'arrayBuffer':
|
package/src/mjs/sanitizer.js
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
parseURLEncodedNumCharRef, URISchemes
|
|
11
11
|
} from './uri-util.js';
|
|
12
12
|
import {
|
|
13
|
-
HEX, REG_DATA_URL, REG_DATA_URL_B64, REG_DATA_URL_G,
|
|
14
|
-
REG_SCRIPT_BLOB, REG_TAG_QUOT
|
|
13
|
+
HEX, MAX_BLOB_SIZE, REG_DATA_URL, REG_DATA_URL_B64, REG_DATA_URL_G,
|
|
14
|
+
REG_MIME_DOM, REG_SCRIPT_BLOB, REG_TAG_QUOT
|
|
15
15
|
} from './constant.js';
|
|
16
16
|
|
|
17
17
|
/* typedef */
|
|
@@ -36,6 +36,18 @@ import {
|
|
|
36
36
|
* @property {string} [hash] - fragment identifier of the sanitized URL
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Internal debug logger.
|
|
41
|
+
* @param {boolean} isDebug - Debug flag.
|
|
42
|
+
* @param {string} message - Output message.
|
|
43
|
+
* @param {Error} [error] - Caught error object.
|
|
44
|
+
*/
|
|
45
|
+
export const logDebug = (isDebug, message, error) => {
|
|
46
|
+
if (isDebug) {
|
|
47
|
+
console.warn(`[URLSanitizer Debug] ${message}`, error ? error.message : '');
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
39
51
|
/**
|
|
40
52
|
* URL sanitizer
|
|
41
53
|
*/
|
|
@@ -99,13 +111,26 @@ class URLSanitizer extends URISchemes {
|
|
|
99
111
|
if (!isString(dom)) {
|
|
100
112
|
throw new TypeError(`Expected String but got ${getType(dom)}.`);
|
|
101
113
|
}
|
|
102
|
-
let
|
|
114
|
+
let decodedDom = dom;
|
|
115
|
+
try {
|
|
116
|
+
// prevent URIError caused by malformed percent-encoding
|
|
117
|
+
// e.g., a standalone '%'
|
|
118
|
+
decodedDom = decodeURIComponent(dom);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// fall through
|
|
121
|
+
}
|
|
122
|
+
let purifiedDom = domPurify.sanitize(decodedDom);
|
|
103
123
|
if (purifiedDom && REG_DATA_URL.test(purifiedDom)) {
|
|
104
124
|
purifiedDom = this.replace(purifiedDom);
|
|
105
125
|
}
|
|
106
126
|
purifiedDom = purifiedDom.replace(/(?:#|%23)$/, '')
|
|
107
127
|
.replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/, '');
|
|
108
|
-
|
|
128
|
+
try {
|
|
129
|
+
// prevent URIError caused by lone surrogates
|
|
130
|
+
return encodeURI(purifiedDom);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return purifiedDom;
|
|
133
|
+
}
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
/**
|
|
@@ -118,6 +143,8 @@ class URLSanitizer extends URISchemes {
|
|
|
118
143
|
* @param {Array.<string>} [opt.allow] - array of allowed schemes
|
|
119
144
|
* @param {Array.<string>} [opt.deny] - array of denied schemes
|
|
120
145
|
* @param {Array.<string>} [opt.only] - array of specific schemes to allow
|
|
146
|
+
* @param {boolean} [opt.allowRelative] - allow relative URLs
|
|
147
|
+
* @param {boolean} [opt.debug] - enable debug mode
|
|
121
148
|
* @returns {?string} - sanitized URL
|
|
122
149
|
*/
|
|
123
150
|
sanitize(url, opt) {
|
|
@@ -125,7 +152,9 @@ class URLSanitizer extends URISchemes {
|
|
|
125
152
|
this.#nest = 0;
|
|
126
153
|
throw new Error('Data URLs nested too deeply.');
|
|
127
154
|
}
|
|
128
|
-
const {
|
|
155
|
+
const {
|
|
156
|
+
allow, deny, only, allowRelative = false, debug = false
|
|
157
|
+
} = opt ?? {};
|
|
129
158
|
const schemeMap = new Map([
|
|
130
159
|
['blob', false],
|
|
131
160
|
['data', false],
|
|
@@ -151,7 +180,8 @@ class URLSanitizer extends URISchemes {
|
|
|
151
180
|
try {
|
|
152
181
|
super.add(item);
|
|
153
182
|
} catch (e) {
|
|
154
|
-
|
|
183
|
+
const msg = `Failed to add scheme '${item}' in 'only' list.`;
|
|
184
|
+
logDebug(debug, msg, e);
|
|
155
185
|
}
|
|
156
186
|
if (super.has(item)) {
|
|
157
187
|
schemeMap.set(item, true);
|
|
@@ -177,7 +207,8 @@ class URLSanitizer extends URISchemes {
|
|
|
177
207
|
try {
|
|
178
208
|
super.add(item);
|
|
179
209
|
} catch (e) {
|
|
180
|
-
|
|
210
|
+
const msg = `Failed to add scheme '${item}' in 'allow' list.`;
|
|
211
|
+
logDebug(debug, msg, e);
|
|
181
212
|
}
|
|
182
213
|
if (super.has(item)) {
|
|
183
214
|
schemeMap.set(item, true);
|
|
@@ -201,25 +232,47 @@ class URLSanitizer extends URISchemes {
|
|
|
201
232
|
}
|
|
202
233
|
}
|
|
203
234
|
let sanitizedUrl;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
235
|
+
let isVerified = super.verify(url);
|
|
236
|
+
let isRelative = false;
|
|
237
|
+
if (!isVerified && allowRelative) {
|
|
238
|
+
try {
|
|
239
|
+
const { hostname, protocol } = new URL(url, 'http://dummy.local');
|
|
240
|
+
if (protocol === 'http:' && hostname === 'dummy.local') {
|
|
241
|
+
isVerified = true;
|
|
242
|
+
isRelative = true;
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
logDebug(debug, 'Failed to parse relative URL.', e);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (isVerified) {
|
|
249
|
+
let hash, href, pathname, protocol, search;
|
|
250
|
+
let scheme = '';
|
|
251
|
+
let schemeParts = [];
|
|
252
|
+
let bool = false;
|
|
253
|
+
if (isRelative) {
|
|
254
|
+
bool = true;
|
|
211
255
|
} else {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
256
|
+
const urlObj = new URL(url);
|
|
257
|
+
({ hash, href, pathname, protocol, search } = urlObj);
|
|
258
|
+
scheme = protocol.replace(/:$/, '');
|
|
259
|
+
schemeParts = scheme.split('+');
|
|
260
|
+
if (restrictScheme) {
|
|
261
|
+
bool = schemeParts.every(s => schemeMap.get(s));
|
|
262
|
+
} else {
|
|
263
|
+
for (const [key, value] of schemeMap.entries()) {
|
|
264
|
+
bool =
|
|
265
|
+
value || (scheme !== key && schemeParts.every(s => s !== key));
|
|
266
|
+
if (!bool) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
216
269
|
}
|
|
217
270
|
}
|
|
218
271
|
}
|
|
219
272
|
if (bool) {
|
|
220
|
-
const isDataUrl = schemeParts.includes('data');
|
|
273
|
+
const isDataUrl = isRelative ? false : schemeParts.includes('data');
|
|
221
274
|
let finalize;
|
|
222
|
-
let urlToSanitize = href;
|
|
275
|
+
let urlToSanitize = isRelative ? url : href;
|
|
223
276
|
if (isDataUrl) {
|
|
224
277
|
const [mediaType, ...dataParts] = pathname.split(',');
|
|
225
278
|
const data = `${dataParts.join(',')}${search}${hash}`;
|
|
@@ -237,7 +290,8 @@ class URLSanitizer extends URISchemes {
|
|
|
237
290
|
urlToSanitize = '';
|
|
238
291
|
}
|
|
239
292
|
} catch (e) {
|
|
240
|
-
|
|
293
|
+
const msg = 'Failed to parse inner data URL protocol.';
|
|
294
|
+
logDebug(debug, msg, e);
|
|
241
295
|
}
|
|
242
296
|
const containsDataUrl = REG_DATA_URL.test(parsedData);
|
|
243
297
|
if (parsedData !== data || containsDataUrl) {
|
|
@@ -383,8 +437,15 @@ const urlSanitizer = new URLSanitizer();
|
|
|
383
437
|
export const sanitizeURL = async (url, opt = {
|
|
384
438
|
allow: [],
|
|
385
439
|
deny: [],
|
|
386
|
-
only: []
|
|
440
|
+
only: [],
|
|
441
|
+
allowRelative: false,
|
|
442
|
+
debug: false,
|
|
443
|
+
maxBlobSize: MAX_BLOB_SIZE
|
|
387
444
|
}) => {
|
|
445
|
+
const isDebug = !!opt?.debug;
|
|
446
|
+
const maxBlobSize = Number.isInteger(opt?.maxBlobSize) && opt.maxBlobSize > 0
|
|
447
|
+
? opt.maxBlobSize
|
|
448
|
+
: MAX_BLOB_SIZE;
|
|
388
449
|
let res;
|
|
389
450
|
if (url && isString(url)) {
|
|
390
451
|
let scheme;
|
|
@@ -392,7 +453,7 @@ export const sanitizeURL = async (url, opt = {
|
|
|
392
453
|
const { protocol } = new URL(url);
|
|
393
454
|
scheme = protocol.replace(/:$/, '');
|
|
394
455
|
} catch (e) {
|
|
395
|
-
|
|
456
|
+
logDebug(isDebug, `Invalid URL input format: ${url}`, e);
|
|
396
457
|
}
|
|
397
458
|
if (scheme === 'blob') {
|
|
398
459
|
const { allow, deny, only } = opt;
|
|
@@ -401,10 +462,12 @@ export const sanitizeURL = async (url, opt = {
|
|
|
401
462
|
(Array.isArray(only) && only.includes('blob'))) {
|
|
402
463
|
let data;
|
|
403
464
|
try {
|
|
404
|
-
data =
|
|
405
|
-
|
|
465
|
+
data = await fetch(url)
|
|
466
|
+
.then(r => r.blob())
|
|
467
|
+
.then(b => createDataURLFromBlob(b, maxBlobSize));
|
|
406
468
|
} catch (e) {
|
|
407
|
-
|
|
469
|
+
const msg = `Failed to fetch and convert blob URL: ${url}`;
|
|
470
|
+
logDebug(isDebug, msg, e);
|
|
408
471
|
}
|
|
409
472
|
if (data) {
|
|
410
473
|
if (Array.isArray(only)) {
|
|
@@ -424,7 +487,7 @@ export const sanitizeURL = async (url, opt = {
|
|
|
424
487
|
}
|
|
425
488
|
}
|
|
426
489
|
URL.revokeObjectURL(url);
|
|
427
|
-
} else if (scheme) {
|
|
490
|
+
} else if (scheme || opt.allowRelative) {
|
|
428
491
|
res = urlSanitizer.sanitize(url, opt);
|
|
429
492
|
}
|
|
430
493
|
}
|
|
@@ -450,11 +513,12 @@ export const sanitizeURLSync = (url, opt) => {
|
|
|
450
513
|
const { protocol } = new URL(url);
|
|
451
514
|
scheme = protocol.replace(/:$/, '');
|
|
452
515
|
} catch (e) {
|
|
453
|
-
|
|
516
|
+
const msg = `Failed to parse URL: ${url}`;
|
|
517
|
+
logDebug(opt?.debug, msg, e);
|
|
454
518
|
}
|
|
455
519
|
if (scheme === 'blob') {
|
|
456
520
|
URL.revokeObjectURL(url);
|
|
457
|
-
} else if (scheme) {
|
|
521
|
+
} else if (scheme || opt?.allowRelative) {
|
|
458
522
|
res = urlSanitizer.sanitize(url, opt);
|
|
459
523
|
}
|
|
460
524
|
}
|
package/src/mjs/uri-util.js
CHANGED
|
@@ -8,7 +8,8 @@ import { FileReader } from './file-reader.js';
|
|
|
8
8
|
import { textChars, uriSchemes } from './lib-util.js';
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
HEX, REG_B64, REG_NUM_REF, REG_SCHEME, REG_SCHEME_EXT,
|
|
11
|
+
HEX, MAX_BLOB_SIZE, REG_B64, REG_NUM_REF, REG_SCHEME, REG_SCHEME_EXT,
|
|
12
|
+
REG_SCRIPT, REG_URL_ENC
|
|
12
13
|
} from './constant.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -128,15 +129,25 @@ export const parseURLEncodedNumCharRef = (str, nest = 0) => {
|
|
|
128
129
|
/**
|
|
129
130
|
* create data URL from blob
|
|
130
131
|
* @param {object} blob - blob
|
|
132
|
+
* @param {number} maxBlobSize - max blob size
|
|
131
133
|
* @returns {Promise.<?string>} - data URL
|
|
132
134
|
*/
|
|
133
|
-
export const createDataURLFromBlob = blob
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
135
|
+
export const createDataURLFromBlob = (blob, maxBlobSize = MAX_BLOB_SIZE) =>
|
|
136
|
+
new Promise((resolve, reject) => {
|
|
137
|
+
if (!Number.isInteger(blob?.size)) {
|
|
138
|
+
return resolve(null);
|
|
139
|
+
} else if (Number.isInteger(maxBlobSize) && blob.size > maxBlobSize) {
|
|
140
|
+
return reject(new DOMException(
|
|
141
|
+
`Blob size (${blob.size} bytes) exceeds the maximum allowed size of ${maxBlobSize} bytes.`,
|
|
142
|
+
'NotReadableError'
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
const reader = new FileReader();
|
|
146
|
+
reader.addEventListener('error', () => reject(reader.error));
|
|
147
|
+
reader.addEventListener('abort', () => resolve(reader.result));
|
|
148
|
+
reader.addEventListener('load', () => resolve(reader.result));
|
|
149
|
+
reader.readAsDataURL(blob);
|
|
150
|
+
});
|
|
140
151
|
|
|
141
152
|
/**
|
|
142
153
|
* URI schemes
|
package/types/mjs/constant.d.ts
CHANGED
package/types/mjs/sanitizer.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export function logDebug(isDebug: boolean, message: string, error?: Error): void;
|
|
1
2
|
export function sanitizeURL(url: string, opt?: {
|
|
2
3
|
allow?: string[] | undefined;
|
|
3
4
|
deny?: string[] | undefined;
|
|
@@ -40,6 +41,8 @@ export class URLSanitizer extends URISchemes {
|
|
|
40
41
|
allow?: string[] | undefined;
|
|
41
42
|
deny?: string[] | undefined;
|
|
42
43
|
only?: string[] | undefined;
|
|
44
|
+
allowRelative?: boolean | undefined;
|
|
45
|
+
debug?: boolean | undefined;
|
|
43
46
|
}): string | null;
|
|
44
47
|
parse(url: string, opt?: {
|
|
45
48
|
allow?: string[] | undefined;
|
package/types/mjs/uri-util.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export function getURLEncodedString(str: string): string;
|
|
|
2
2
|
export function escapeURLEncodedHTMLChars(ch: string): string;
|
|
3
3
|
export function parseBase64(data: string): string;
|
|
4
4
|
export function parseURLEncodedNumCharRef(str: string, nest?: number): string;
|
|
5
|
-
export function createDataURLFromBlob(blob: object): Promise<string | null>;
|
|
5
|
+
export function createDataURLFromBlob(blob: object, maxBlobSize?: number): Promise<string | null>;
|
|
6
6
|
export class URISchemes {
|
|
7
7
|
get(): Array<string>;
|
|
8
8
|
has(scheme: string): boolean;
|