url-sanitizer 2.0.13 → 2.2.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/package.json CHANGED
@@ -15,6 +15,7 @@
15
15
  "types"
16
16
  ],
17
17
  "type": "module",
18
+ "types": "./types/index.d.ts",
18
19
  "exports": {
19
20
  ".": {
20
21
  "types": "./types/index.d.ts",
@@ -24,12 +25,12 @@
24
25
  "./package.json": "./package.json"
25
26
  },
26
27
  "dependencies": {
27
- "dompurify": "^3.4.2",
28
+ "dompurify": "^3.4.3",
28
29
  "jsdom": "^29.1.1"
29
30
  },
30
31
  "devDependencies": {
31
- "@types/jsdom": "^28.0.1",
32
- "@types/node": "^25.6.2",
32
+ "@types/jsdom": "^28.0.3",
33
+ "@types/node": "^25.8.0",
33
34
  "c8": "^11.0.0",
34
35
  "chai": "^6.2.2",
35
36
  "commander": "^14.0.3",
@@ -78,5 +79,5 @@
78
79
  "test:bdd": "http-server ./ -c-1 -o test/bdd/url-sanitizer.test.html",
79
80
  "tsc": "node scripts/index clean --dir=types -i && npx tsc"
80
81
  },
81
- "version": "2.0.13"
82
+ "version": "2.2.0"
82
83
  }
@@ -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","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"]
@@ -3,7 +3,9 @@
3
3
  */
4
4
 
5
5
  /* numeric */
6
+ export const CHUNK_SIZE = 8 * 1024;
6
7
  export const HEX = 16;
8
+ export const MAX_BLOB_SIZE = 32 * 1024 * 1024;
7
9
  export const TYPE_FROM = 8;
8
10
  export const TYPE_TO = -1;
9
11
 
@@ -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 { REG_CHARSET, REG_MIME_DOM, REG_MIME_TEXT } from './constant.js';
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 binary = String.fromCharCode(...uint8arr);
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':
@@ -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, REG_MIME_DOM,
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
  */
@@ -125,7 +137,7 @@ class URLSanitizer extends URISchemes {
125
137
  this.#nest = 0;
126
138
  throw new Error('Data URLs nested too deeply.');
127
139
  }
128
- const { allow, deny, only } = opt ?? {};
140
+ const { allow, deny, only, debug = false } = opt ?? {};
129
141
  const schemeMap = new Map([
130
142
  ['blob', false],
131
143
  ['data', false],
@@ -151,7 +163,8 @@ class URLSanitizer extends URISchemes {
151
163
  try {
152
164
  super.add(item);
153
165
  } catch (e) {
154
- // fall through
166
+ const msg = `Failed to add scheme '${item}' in 'only' list.`;
167
+ logDebug(debug, msg, e);
155
168
  }
156
169
  if (super.has(item)) {
157
170
  schemeMap.set(item, true);
@@ -177,7 +190,8 @@ class URLSanitizer extends URISchemes {
177
190
  try {
178
191
  super.add(item);
179
192
  } catch (e) {
180
- // fall through
193
+ const msg = `Failed to add scheme '${item}' in 'allow' list.`;
194
+ logDebug(debug, msg, e);
181
195
  }
182
196
  if (super.has(item)) {
183
197
  schemeMap.set(item, true);
@@ -237,7 +251,8 @@ class URLSanitizer extends URISchemes {
237
251
  urlToSanitize = '';
238
252
  }
239
253
  } catch (e) {
240
- // fall through
254
+ const msg = 'Failed to parse inner data URL protocol.';
255
+ logDebug(debug, msg, e);
241
256
  }
242
257
  const containsDataUrl = REG_DATA_URL.test(parsedData);
243
258
  if (parsedData !== data || containsDataUrl) {
@@ -383,8 +398,14 @@ const urlSanitizer = new URLSanitizer();
383
398
  export const sanitizeURL = async (url, opt = {
384
399
  allow: [],
385
400
  deny: [],
386
- only: []
401
+ only: [],
402
+ debug: false,
403
+ maxBlobSize: MAX_BLOB_SIZE
387
404
  }) => {
405
+ const isDebug = !!opt?.debug;
406
+ const maxBlobSize = Number.isInteger(opt?.maxBlobSize) && opt.maxBlobSize > 0
407
+ ? opt.maxBlobSize
408
+ : MAX_BLOB_SIZE;
388
409
  let res;
389
410
  if (url && isString(url)) {
390
411
  let scheme;
@@ -392,7 +413,7 @@ export const sanitizeURL = async (url, opt = {
392
413
  const { protocol } = new URL(url);
393
414
  scheme = protocol.replace(/:$/, '');
394
415
  } catch (e) {
395
- // fall through;
416
+ logDebug(isDebug, `Invalid URL input format: ${url}`, e);
396
417
  }
397
418
  if (scheme === 'blob') {
398
419
  const { allow, deny, only } = opt;
@@ -401,10 +422,12 @@ export const sanitizeURL = async (url, opt = {
401
422
  (Array.isArray(only) && only.includes('blob'))) {
402
423
  let data;
403
424
  try {
404
- data =
405
- await fetch(url).then(r => r.blob()).then(createDataURLFromBlob);
425
+ data = await fetch(url)
426
+ .then(r => r.blob())
427
+ .then(b => createDataURLFromBlob(b, maxBlobSize));
406
428
  } catch (e) {
407
- // fall through
429
+ const msg = `Failed to fetch and convert blob URL: ${url}`;
430
+ logDebug(isDebug, msg, e);
408
431
  }
409
432
  if (data) {
410
433
  if (Array.isArray(only)) {
@@ -450,7 +473,8 @@ export const sanitizeURLSync = (url, opt) => {
450
473
  const { protocol } = new URL(url);
451
474
  scheme = protocol.replace(/:$/, '');
452
475
  } catch (e) {
453
- // fall through;
476
+ const msg = `Failed to parse URL: ${url}`;
477
+ logDebug(opt?.debug, msg, e);
454
478
  }
455
479
  if (scheme === 'blob') {
456
480
  URL.revokeObjectURL(url);
@@ -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, REG_SCRIPT, REG_URL_ENC
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 => new Promise((resolve, reject) => {
134
- const reader = new FileReader();
135
- reader.addEventListener('error', () => reject(reader.error));
136
- reader.addEventListener('abort', () => resolve(reader.result));
137
- reader.addEventListener('load', () => resolve(reader.result));
138
- reader.readAsDataURL(blob);
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
@@ -1,4 +1,6 @@
1
+ export const CHUNK_SIZE: number;
1
2
  export const HEX: 16;
3
+ export const MAX_BLOB_SIZE: number;
2
4
  export const TYPE_FROM: 8;
3
5
  export const TYPE_TO: -1;
4
6
  export const REG_B64: RegExp;
@@ -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;
@@ -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;