url-sanitizer 4.1.4 → 4.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
@@ -39,7 +39,7 @@
39
39
  "esbuild": "^0.28.1",
40
40
  "eslint": "^9.39.5",
41
41
  "eslint-config-prettier": "^10.1.8",
42
- "eslint-plugin-jsdoc": "^63.0.14",
42
+ "eslint-plugin-jsdoc": "^63.2.2",
43
43
  "eslint-plugin-prettier": "^5.5.6",
44
44
  "eslint-plugin-regexp": "^3.1.1",
45
45
  "eslint-plugin-unicorn": "^64.0.0",
@@ -50,23 +50,22 @@
50
50
  "mocha": "^11.7.6",
51
51
  "neostandard": "^0.13.0",
52
52
  "papaparse": "^5.5.4",
53
- "prettier": "^3.9.5",
54
- "sinon": "^22.0.0",
53
+ "prettier": "^3.9.6",
54
+ "sinon": "^22.1.0",
55
55
  "strict-url-sanitise": "^0.0.1",
56
56
  "typescript": "^7.0.2",
57
- "undici": "^7.28.0"
57
+ "undici": "^7.29.0"
58
58
  },
59
59
  "overrides": {
60
60
  "c8": {
61
61
  "yargs": "^18.0.0"
62
62
  },
63
63
  "eslint": {
64
- "brace-expansion": "^1.1.13",
65
- "js-yaml": "^4.2.0"
64
+ "js-yaml": "^4.3.0"
66
65
  },
67
66
  "mocha": {
68
67
  "diff": "^8.0.3",
69
- "js-yaml": "^4.2.0"
68
+ "js-yaml": "^4.3.0"
70
69
  },
71
70
  "serialize-javascript": "^7.0.4"
72
71
  },
@@ -94,5 +93,5 @@
94
93
  "engines": {
95
94
  "node": ">=22.13"
96
95
  },
97
- "version": "4.1.4"
96
+ "version": "4.2.0"
98
97
  }
@@ -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","ez","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","mdoc","mdoc-openid4vp","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","musik","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","npamp","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","sss","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","tttps","turn","turns","tv","udp","unreal","upn","urn","ut2004","uuaid","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"]
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","ez","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","mdoc","mdoc-openid4vp","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","musik","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","nostr","notes","npamp","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","sss","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","tttps","turn","turns","tv","udp","unreal","upn","urn","ust","ut2004","uuaid","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"]
@@ -10,6 +10,6 @@ const { window } = new JSDOM('');
10
10
  /**
11
11
  * An instance of DOMPurify initialized with a JSDOM window context.
12
12
  * Used to sanitize HTML/SVG strings.
13
- * @type {import('dompurify')}
13
+ * @type {import('dompurify').DOMPurify}
14
14
  */
15
15
  export const domPurify = DOMPurify(window);
package/src/mjs/regexp.js CHANGED
@@ -80,9 +80,16 @@ export const REG_URL_ENC = /^%[\dA-F]{2}$/i;
80
80
 
81
81
  /**
82
82
  * Matches an ampersand-encoded character (&foo).
83
+ * @type {RegExp}
83
84
  */
84
85
  export const REG_AMP_ENC = /&(?=[#a-z0-9])(?![a-z0-9]+=)/i;
85
86
 
87
+ /**
88
+ * Match one or more consecutive URL percent-encoded sequences (%20, %E3%81%82).
89
+ * @type {RegExp}
90
+ */
91
+ export const REG_PCT_ENC = /(?:%[\dA-F]{2})+/gi;
92
+
86
93
  /**
87
94
  * Validates relative URLs and flags invalid patterns.
88
95
  * @type {RegExp}
@@ -126,10 +126,8 @@ class URLSanitizer extends URISchemes {
126
126
  if (!sanitizer || !ctx) {
127
127
  return;
128
128
  }
129
- let urlObj;
130
- try {
131
- urlObj = new URL(evt.attrValue);
132
- } catch {
129
+ const urlObj = URL.parse(evt.attrValue);
130
+ if (!urlObj) {
133
131
  return;
134
132
  }
135
133
  if (urlObj.protocol === 'data:') {
@@ -357,10 +355,10 @@ class URLSanitizer extends URISchemes {
357
355
  let relativeParsedPath = '';
358
356
  // Handle Relative URLs
359
357
  if (!isVerified && allowRelative && !REG_VERIFY_RELATIVE.test(url)) {
360
- try {
361
- const dummyUrl = new URL(url, 'http://dummy.local');
362
- const normUrl = url.normalize('NFKC');
363
- const dummyUrlNormalized = new URL(normUrl, 'http://dummy.local');
358
+ const normUrl = url.normalize('NFKC');
359
+ const dummyUrl = URL.parse(url, 'http://dummy.local');
360
+ const dummyUrlNormalized = URL.parse(normUrl, 'http://dummy.local');
361
+ if (dummyUrl && dummyUrlNormalized) {
364
362
  if (
365
363
  dummyUrl.protocol === 'http:' &&
366
364
  dummyUrl.hostname === 'dummy.local' &&
@@ -370,8 +368,8 @@ class URLSanitizer extends URISchemes {
370
368
  isRelative = true;
371
369
  relativeParsedPath = `${dummyUrl.pathname}${dummyUrl.search}${dummyUrl.hash}`;
372
370
  }
373
- } catch (e) {
374
- logDebug(ctx.debug, 'Failed to parse relative URL.', e);
371
+ } else {
372
+ logDebug(ctx.debug, 'Failed to parse relative URL.');
375
373
  }
376
374
  }
377
375
  if (!isVerified) {
@@ -388,7 +386,7 @@ class URLSanitizer extends URISchemes {
388
386
  urlToSanitize: relativeParsedPath
389
387
  };
390
388
  }
391
- const urlObj = new URL(url);
389
+ const urlObj = URL.parse(url);
392
390
  const scheme = urlObj.protocol.replace(/:$/, '').normalize('NFKC');
393
391
  const schemeParts = scheme.split('+');
394
392
  return {
@@ -455,14 +453,18 @@ class URLSanitizer extends URISchemes {
455
453
  const decodedData = parseURLEncodedNumCharRef(parsedData).trim();
456
454
  const normalizedData = decodedData.normalize('NFKC');
457
455
  const dummy = 'http://dummy.local';
458
- const { protocol: dataScheme } = new URL(normalizedData, dummy);
456
+ const parsedUrl = URL.parse(normalizedData, dummy);
457
+ if (!parsedUrl) {
458
+ logDebug(ctx.debug, 'Failed to parse inner data URL protocol.');
459
+ return null;
460
+ }
461
+ const dataScheme = parsedUrl.protocol;
459
462
  const dataSchemeParts = dataScheme.replace(/:$/, '').split('+');
460
463
  if (dataSchemeParts.some(s => REG_SCRIPT_BLOB.test(s))) {
461
464
  return null;
462
465
  }
463
466
  } catch (e) {
464
- const msg = 'Failed to parse inner data URL protocol.';
465
- logDebug(ctx.debug, msg, e);
467
+ logDebug(ctx.debug, 'Failed to parse inner data URL protocol.', e);
466
468
  return null;
467
469
  }
468
470
  if (!mediaType || REG_MIME_DOM.test(mediaType)) {
@@ -531,12 +533,11 @@ class URLSanitizer extends URISchemes {
531
533
  !REG_TAG_QUOT.test(url) &&
532
534
  !url.includes('data:')
533
535
  ) {
534
- try {
535
- const { href } = new URL(url);
536
- return href.replace(/%26/g, escapeURLEncodedHTMLChars);
537
- } catch (e) {
538
- const msg = `Failed to parse URL.`;
539
- logDebug(opt?.debug, msg, e);
536
+ const parsedUrl = URL.parse(url);
537
+ if (parsedUrl) {
538
+ return parsedUrl.href.replace(/%26/g, escapeURLEncodedHTMLChars);
539
+ } else {
540
+ logDebug(opt?.debug, `Failed to parse URL.`);
540
541
  return null;
541
542
  }
542
543
  }
@@ -744,11 +745,11 @@ export const sanitizeURL = async (
744
745
  let res;
745
746
  if (url && isString(url)) {
746
747
  let scheme;
747
- try {
748
- const { protocol } = new URL(url);
749
- scheme = protocol.replace(/:$/, '');
750
- } catch (e) {
751
- logDebug(isDebug, `Invalid URL input format: ${url}`, e);
748
+ const parsedUrl = URL.parse(url);
749
+ if (parsedUrl) {
750
+ scheme = parsedUrl.protocol.replace(/:$/, '');
751
+ } else {
752
+ logDebug(isDebug, `Invalid URL input format: ${url}`);
752
753
  }
753
754
  if (scheme === 'blob') {
754
755
  const { allow, deny, only } = opt;
@@ -824,12 +825,11 @@ export const sanitizeURLSync = (
824
825
  let res;
825
826
  if (url && isString(url)) {
826
827
  let scheme;
827
- try {
828
- const { protocol } = new URL(url);
829
- scheme = protocol.replace(/:$/, '');
830
- } catch (e) {
831
- const msg = `Invalid URL input format: ${url}`;
832
- logDebug(isDebug, msg, e);
828
+ const parsedUrl = URL.parse(url);
829
+ if (parsedUrl) {
830
+ scheme = parsedUrl.protocol.replace(/:$/, '');
831
+ } else {
832
+ logDebug(isDebug, `Invalid URL input format: ${url}`);
833
833
  }
834
834
  if (scheme === 'blob') {
835
835
  if (opt?.revokeObjectURL) {
@@ -854,11 +854,11 @@ export const inspectURL = async url => {
854
854
  };
855
855
 
856
856
  /**
857
- * @deprecated Use inspectURL instead.
857
+ * @deprecated Use {@link inspectURL} instead.
858
858
  * @param {string} url - The URL string to inspect.
859
859
  * @returns {Promise<InspectedURLResult>} A promise resolving to the inspected URL result.
860
860
  */
861
- export const parseURL = url => inspectURL(url);
861
+ export const parseURL = async url => inspectURL(url);
862
862
 
863
863
  /**
864
864
  * Inspects, parses, and sanitizes the given URL synchronously.
@@ -868,7 +868,7 @@ export const parseURL = url => inspectURL(url);
868
868
  export const inspectURLSync = url => urlSanitizer.inspect(url);
869
869
 
870
870
  /**
871
- * @deprecated Use inspectURLSync instead.
871
+ * @deprecated Use {@link inspectURLSync} instead.
872
872
  * @param {string} url - The URL string to inspect.
873
873
  * @returns {InspectedURLResult} The inspected URL result.
874
874
  */
@@ -11,6 +11,7 @@ import { CHUNK_SIZE, DECI, HEX, MAX_BLOB_SIZE, MAX_NEST } from './constant.js';
11
11
  import {
12
12
  REG_HASH,
13
13
  REG_NUM_REF,
14
+ REG_PCT_ENC,
14
15
  REG_QUERY,
15
16
  REG_SCHEME_EXT,
16
17
  REG_SCRIPT,
@@ -88,18 +89,18 @@ export class URISchemes {
88
89
  if (!isString(uri)) {
89
90
  return false;
90
91
  }
91
- try {
92
- const { protocol } = new URL(uri.normalize('NFKC'));
93
- const scheme = protocol.replace(/:$/, '');
94
- const parts = scheme.split('+');
95
- const isScript = parts.some(s => REG_SCRIPT.test(s));
96
- if (isScript) {
97
- return false;
98
- }
99
- return REG_SCHEME_EXT.test(scheme) || parts.every(s => schemes.has(s));
100
- } catch {
92
+ const normalizedUri = uri.normalize('NFKC');
93
+ const parsedUrl = URL.parse(normalizedUri);
94
+ if (!parsedUrl) {
95
+ return false;
96
+ }
97
+ const scheme = parsedUrl.protocol.replace(/:$/, '');
98
+ const parts = scheme.split('+');
99
+ const isScript = parts.some(s => REG_SCRIPT.test(s));
100
+ if (isScript) {
101
101
  return false;
102
102
  }
103
+ return REG_SCHEME_EXT.test(scheme) || parts.every(s => schemes.has(s));
103
104
  }
104
105
  }
105
106
 
@@ -113,11 +114,11 @@ export const getURLEncodedString = str => {
113
114
  throw new TypeError(`Expected String but got ${getType(str)}.`);
114
115
  }
115
116
  const bytes = encoder.encode(str);
116
- const encoded = [];
117
+ let encoded = '';
117
118
  for (let i = 0; i < bytes.length; i++) {
118
- encoded.push(HEX_TABLE[bytes[i]]);
119
+ encoded += HEX_TABLE[bytes[i]];
119
120
  }
120
- return encoded.join('');
121
+ return encoded;
121
122
  };
122
123
 
123
124
  /**
@@ -226,7 +227,7 @@ export const parseURLEncodedNumCharRef = (str, nest = 0) => {
226
227
  res = decodeURIComponent(str);
227
228
  } catch {
228
229
  // Fallback: decode only valid multi-byte %XX sequences.
229
- res = str.replace(/(?:%[\dA-F]{2})+/gi, match => {
230
+ res = str.replace(REG_PCT_ENC, match => {
230
231
  try {
231
232
  return decodeURIComponent(match);
232
233
  } catch {
@@ -1 +1 @@
1
- export declare const domPurify: import('dompurify');
1
+ export declare const domPurify: import('dompurify').DOMPurify;
@@ -11,4 +11,5 @@ export declare const REG_SCRIPT_BLOB: RegExp;
11
11
  export declare const REG_TAG_QUOT: RegExp;
12
12
  export declare const REG_URL_ENC: RegExp;
13
13
  export declare const REG_AMP_ENC: RegExp;
14
+ export declare const REG_PCT_ENC: RegExp;
14
15
  export declare const REG_VERIFY_RELATIVE: RegExp;