url-sanitizer
Advanced tools
Comparing version 0.5.5 to 0.5.7
@@ -977,14 +977,14 @@ // src/mjs/common.js | ||
* @property {string} data.data - data part of the data URL | ||
* @property {string} href - same as URL API | ||
* @property {string} origin - same as URL API | ||
* @property {string} protocol - same as URL API | ||
* @property {string} username - same as URL API | ||
* @property {string} password - same as URL API | ||
* @property {string} host - same as URL API | ||
* @property {string} hostname - same as URL API | ||
* @property {string} port - same as URL API | ||
* @property {string} pathname - same as URL API | ||
* @property {string} search - same as URL API | ||
* @property {object} searchParams - same as URL API | ||
* @property {string} hash - same as URL API | ||
* @property {string} href - sanitized URL input | ||
* @property {string} origin - scheme, domain and port of the sanitized URL | ||
* @property {string} protocol - protocol scheme of the sanitized URL | ||
* @property {string} username - username specified before the domain name | ||
* @property {string} password - password specified before the domain name | ||
* @property {string} host - domain and port of the sanitized URL | ||
* @property {string} hostname - domain of the sanitized URL | ||
* @property {string} port - port number of the sanitized URL | ||
* @property {string} pathname - path of the sanitized URL | ||
* @property {string} search - query string of the sanitized URL | ||
* @property {object} searchParams - URLSearchParams object | ||
* @property {string} hash - fragment identifier of the sanitized URL | ||
*/ | ||
@@ -995,9 +995,10 @@ /** | ||
* @param {string} url - URL input | ||
* @param {object} opt - options | ||
* @returns {ParsedURL} - result with extended props based on URL API | ||
*/ | ||
parse(url) { | ||
parse(url, opt) { | ||
if (!isString(url)) { | ||
throw new TypeError(`Expected String but got ${getType(url)}.`); | ||
} | ||
const sanitizedUrl = this.sanitize(url, { | ||
const sanitizedUrl = this.sanitize(url, opt ?? { | ||
allow: ["data", "file"] | ||
@@ -1047,7 +1048,15 @@ }); | ||
}; | ||
var sanitizeUrl = (url, opt) => urlSanitizer.sanitize(url, opt ?? { | ||
allow: [], | ||
deny: [], | ||
only: [] | ||
}); | ||
var sanitizeUrl = (url, opt) => { | ||
const parsedUrl = urlSanitizer.parse(url, opt ?? { | ||
allow: [], | ||
deny: [], | ||
only: [] | ||
}); | ||
let res; | ||
if (parsedUrl) { | ||
const { href } = parsedUrl; | ||
res = href; | ||
} | ||
return res ?? null; | ||
}; | ||
var sanitizeURL = async (url, opt) => { | ||
@@ -1057,2 +1066,7 @@ const res = await sanitizeUrl(url, opt); | ||
}; | ||
var parseUrl = (url) => urlSanitizer.parse(url); | ||
var parseURL = async (url) => { | ||
const res = await parseUrl(url); | ||
return res; | ||
}; | ||
export { | ||
@@ -1062,2 +1076,4 @@ urlSanitizer as default, | ||
isUri as isURISync, | ||
parseURL, | ||
parseUrl as parseURLSync, | ||
sanitizeURL, | ||
@@ -1064,0 +1080,0 @@ sanitizeUrl as sanitizeURLSync |
@@ -1,2 +0,2 @@ | ||
var g=t=>Object.prototype.toString.call(t).slice(8,-1),d=t=>typeof t=="string"||t instanceof String;var T=[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 O=["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","aim","amss","android","appdata","apt","ar","ark","attachment","aw","barion","beshare","bitcoin","bitcoincash","blob","bolo","browserext","cabal","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cvs","dab","dat","data","dav","diaspora","dict","did","dis","dlna-playcontainer","dlna-playsingle","dns","dntp","doi","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","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","http","https","hxxp","hxxps","hydrazone","hyper","iax","icap","icon","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","lorawan","lpa","lvlt","magnet","mailto","maps","market","matrix","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","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-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-powerpoint","ms-project","ms-publisher","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-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mumble","mupdate","mvn","news","nfs","ni","nih","nntp","notes","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openpgp4fpr","otpauth","palm","paparazzi","payment","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","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spiffe","spotify","ssb","ssh","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","urn","ut2004","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcon","xcon-userid","xfire","xmlrpc.beep","xmlrpc.beeps","xmpp","xri","ymsgr","z39.50r","z39.50s"];var b=16,Q=/^[\da-z+/\-_=]+$/i,V=/data:[^,]*,[^"]+/g,G=/data:[^,]*;?base64,[\da-z+/\-_=]+/i,W=/[<>"'\s]/g,Z=/%(?:2(?:2|7)|3(?:C|E))/g,ee=/&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/ig,te=/^[a-z][\da-z+\-.]*$/,se=/^(?:ext|web)\+[a-z]+$/,x=/(?:java|vb)script/,re=/^%[\dA-F]{2}$/i,ae=/%26/g,L=t=>{if(!d(t))throw new TypeError(`Expected String but got ${g(t)}.`);let e=[];for(let s of t)e.push(`%${s.charCodeAt(0).toString(b).toUpperCase()}`);return e.join("")},N=t=>{if(d(t))if(re.test(t))t=t.toUpperCase();else throw new Error(`Invalid URL encoded character: ${t}`);else throw new TypeError(`Expected String but got ${g(t)}.`);let[e,s,o,n,l,c]=["&","#","<",">",'"',"'"].map(L),p;return t===e?p=`${e}amp;`:t===o?p=`${e}lt;`:t===n?p=`${e}gt;`:t===l?p=`${e}quot;`:t===c?p=`${e}${s}39;`:p=t,p},ie=t=>{if(d(t)){if(!Q.test(t))throw new Error(`Invalid base64 data: ${t}`)}else throw new TypeError(`Expected String but got ${g(t)}.`);let e=atob(t),s=Uint8Array.from([...e].map(l=>l.charCodeAt(0))),o=new Set(T),n;return s.every(l=>o.has(l))?n=e.replace(/\s/g,L):n=t,n},q=(t,e=0)=>{if(!d(t))throw new TypeError(`Expected String but got ${g(t)}.`);if(Number.isInteger(e)){if(e>b)throw new Error("Character references nested too deeply.")}else throw new TypeError(`Expected Number but got ${g(e)}.`);let s=decodeURIComponent(t);if(/&#/.test(s)){let o=new Set(T),n=[...s.matchAll(ee)].reverse();for(let l of n){let[c,p]=l,i;if(/^x[\dA-F]+/i.test(p)?i=parseInt(`0${p}`,b):/^[\d]+/.test(p)&&(i=parseInt(p)),Number.isInteger(i)){let{index:m}=l,[r,a]=[s.substring(0,m),s.substring(m+c.length)];o.has(i)?(s=`${r}${String.fromCharCode(i)}${a}`,(/#x?$/.test(r)||/^#(?:x(?:00)?[2-7]|\d)/.test(a))&&(s=q(s,++e))):i<b*b&&(s=`${r}${a}`)}}}return s},j=class{#e;constructor(){this.#e=new Set(O)}get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(d(e)){if(x.test(e)||!te.test(e))throw new Error(`Invalid scheme: ${e}`)}else throw new TypeError(`Expected String but got ${g(e)}.`);return this.#e.add(e),[...this.#e]}remove(e){return this.#e.delete(e)}isURI(e){let s;if(d(e))try{let{protocol:o}=new URL(e),n=o.replace(/:$/,""),l=n.split("+");s=!x.test(n)&&se.test(n)||l.every(c=>this.#e.has(c))}catch{s=!1}return!!s}},A=class extends j{#e;#t;constructor(){super(),this.#e=0,this.#t=new Set}sanitize(e,s={allow:[],deny:[],only:[]}){if(this.#e>b)throw this.#e=0,new Error("Data URLs nested too deeply.");let{allow:o,deny:n,only:l}=s??{},c=new Map([["data",!1],["file",!1],["javascrpt",!1],["vbscript",!1]]),p=!1;if(Array.isArray(l)&&l.length){let m=super.get();for(let a of m)c.set(a,!1);let r=Object.values(l);for(let a of r)if(d(a)&&(a=a.trim(),!x.test(a))){if(super.has(a))c.set(a,!0);else{try{super.add(a)}catch{}super.has(a)&&c.set(a,!0)}!p&&c.has(a)&&(p=c.get(a))}}else{if(Array.isArray(o)&&o.length){let m=Object.values(o);for(let r of m)if(d(r)&&(r=r.trim(),!x.test(r)))if(super.has(r))c.set(r,!0);else{try{super.add(r)}catch{}super.has(r)&&c.set(r,!0)}}if(Array.isArray(n)&&n.length){let m=Object.values(n);for(let r of m)d(r)&&(r=r.trim(),r&&c.set(r,!1))}}let i;if(super.isURI(e)){let{hash:m,href:r,pathname:a,protocol:w,search:v}=new URL(e),$=w.replace(/:$/,""),R=$.split("+"),E;if(p)E=R.every(f=>c.get(f));else for(let[f,y]of c.entries())if(E=y||$!==f&&R.every(S=>S!==f),!E)break;if(E){let f,y=r;if(R.includes("data")){let[S,...B]=a.split(","),_=`${B.join(",")}${v}${m}`,U=S.split(";"),h=_;if(U[U.length-1]==="base64")U.pop(),h=ie(_);else try{let k=q(h),{protocol:z}=new URL(k.trim());z.replace(/:$/,"").split("+").some(u=>x.test(u))&&(y="")}catch{}let M=/data:[^,]*,/.test(h);if(h!==_||M){if(M){let z=[...h.matchAll(V)].reverse();for(let C of z){let[u]=C;G.test(u)&&([u]=G.exec(u)),this.#e++,this.#t.add(u);let P=this.sanitize(u,{allow:["data"]});if(P){let{index:D}=C,[Y,X]=[h.substring(0,D),h.substring(D+u.length)];h=`${Y}${P}${X}`}}this.#t.has(e)?this.#t.delete(e):f=!0}else this.#t.has(e)?this.#t.delete(e):f=!0;y=`${$}:${U.join(";")},${h}`}else this.#t.has(e)?this.#t.delete(e):f=!0}else f=!0;y?(i=y.replace(W,L).replace(ae,N),f&&(i=i.replace(Z,N),this.#e=0)):(i=y,this.#e=0)}}return i||null}parse(e){if(!d(e))throw new TypeError(`Expected String but got ${g(e)}.`);let s=this.sanitize(e,{allow:["data","file"]}),o=new Map([["input",e]]);if(s){let n=new URL(s),{pathname:l,protocol:c}=n,p=c.replace(/:$/,"").split("+");if(o.set("valid",!0),p.includes("data")){let i=new Map,[m,...r]=l.split(","),a=`${r.join(",")}`,w=m.split(";"),v=w[w.length-1]==="base64";v&&w.pop(),i.set("mime",w.join(";")),i.set("base64",v),i.set("data",a),o.set("data",Object.fromEntries(i))}else o.set("data",null);for(let i in n){let m=n[i];typeof m!="function"&&o.set(i,m)}}else o.set("valid",!1);return Object.fromEntries(o)}},I=new A,H=t=>I.isURI(t),oe=async t=>await H(t),F=(t,e)=>I.sanitize(t,e??{allow:[],deny:[],only:[]}),ne=async(t,e)=>await F(t,e);export{I as default,oe as isURI,H as isURISync,ne as sanitizeURL,F as sanitizeURLSync}; | ||
var g=t=>Object.prototype.toString.call(t).slice(8,-1),d=t=>typeof t=="string"||t instanceof String;var j=[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 O=["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","aim","amss","android","appdata","apt","ar","ark","attachment","aw","barion","beshare","bitcoin","bitcoincash","blob","bolo","browserext","cabal","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cvs","dab","dat","data","dav","diaspora","dict","did","dis","dlna-playcontainer","dlna-playsingle","dns","dntp","doi","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","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","http","https","hxxp","hxxps","hydrazone","hyper","iax","icap","icon","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","lorawan","lpa","lvlt","magnet","mailto","maps","market","matrix","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","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-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-powerpoint","ms-project","ms-publisher","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-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mumble","mupdate","mvn","news","nfs","ni","nih","nntp","notes","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openpgp4fpr","otpauth","palm","paparazzi","payment","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","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spiffe","spotify","ssb","ssh","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","urn","ut2004","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcon","xcon-userid","xfire","xmlrpc.beep","xmlrpc.beeps","xmpp","xri","ymsgr","z39.50r","z39.50s"];var x=16,V=/^[\da-z+/\-_=]+$/i,W=/data:[^,]*,[^"]+/g,G=/data:[^,]*;?base64,[\da-z+/\-_=]+/i,Z=/[<>"'\s]/g,ee=/%(?:2(?:2|7)|3(?:C|E))/g,te=/&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/ig,se=/^[a-z][\da-z+\-.]*$/,re=/^(?:ext|web)\+[a-z]+$/,v=/(?:java|vb)script/,ae=/^%[\dA-F]{2}$/i,ie=/%26/g,I=t=>{if(!d(t))throw new TypeError(`Expected String but got ${g(t)}.`);let e=[];for(let r of t)e.push(`%${r.charCodeAt(0).toString(x).toUpperCase()}`);return e.join("")},N=t=>{if(d(t))if(ae.test(t))t=t.toUpperCase();else throw new Error(`Invalid URL encoded character: ${t}`);else throw new TypeError(`Expected String but got ${g(t)}.`);let[e,r,o,a,n,c]=["&","#","<",">",'"',"'"].map(I),p;return t===e?p=`${e}amp;`:t===o?p=`${e}lt;`:t===a?p=`${e}gt;`:t===n?p=`${e}quot;`:t===c?p=`${e}${r}39;`:p=t,p},oe=t=>{if(d(t)){if(!V.test(t))throw new Error(`Invalid base64 data: ${t}`)}else throw new TypeError(`Expected String but got ${g(t)}.`);let e=atob(t),r=Uint8Array.from([...e].map(n=>n.charCodeAt(0))),o=new Set(j),a;return r.every(n=>o.has(n))?a=e.replace(/\s/g,I):a=t,a},q=(t,e=0)=>{if(!d(t))throw new TypeError(`Expected String but got ${g(t)}.`);if(Number.isInteger(e)){if(e>x)throw new Error("Character references nested too deeply.")}else throw new TypeError(`Expected Number but got ${g(e)}.`);let r=decodeURIComponent(t);if(/&#/.test(r)){let o=new Set(j),a=[...r.matchAll(te)].reverse();for(let n of a){let[c,p]=n,m;if(/^x[\dA-F]+/i.test(p)?m=parseInt(`0${p}`,x):/^[\d]+/.test(p)&&(m=parseInt(p)),Number.isInteger(m)){let{index:l}=n,[s,i]=[r.substring(0,l),r.substring(l+c.length)];o.has(m)?(r=`${s}${String.fromCharCode(m)}${i}`,(/#x?$/.test(s)||/^#(?:x(?:00)?[2-7]|\d)/.test(i))&&(r=q(r,++e))):m<x*x&&(r=`${s}${i}`)}}}return r},A=class{#e;constructor(){this.#e=new Set(O)}get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(d(e)){if(v.test(e)||!se.test(e))throw new Error(`Invalid scheme: ${e}`)}else throw new TypeError(`Expected String but got ${g(e)}.`);return this.#e.add(e),[...this.#e]}remove(e){return this.#e.delete(e)}isURI(e){let r;if(d(e))try{let{protocol:o}=new URL(e),a=o.replace(/:$/,""),n=a.split("+");r=!v.test(a)&&re.test(a)||n.every(c=>this.#e.has(c))}catch{r=!1}return!!r}},L=class extends A{#e;#t;constructor(){super(),this.#e=0,this.#t=new Set}sanitize(e,r={allow:[],deny:[],only:[]}){if(this.#e>x)throw this.#e=0,new Error("Data URLs nested too deeply.");let{allow:o,deny:a,only:n}=r??{},c=new Map([["data",!1],["file",!1],["javascrpt",!1],["vbscript",!1]]),p=!1;if(Array.isArray(n)&&n.length){let l=super.get();for(let i of l)c.set(i,!1);let s=Object.values(n);for(let i of s)if(d(i)&&(i=i.trim(),!v.test(i))){if(super.has(i))c.set(i,!0);else{try{super.add(i)}catch{}super.has(i)&&c.set(i,!0)}!p&&c.has(i)&&(p=c.get(i))}}else{if(Array.isArray(o)&&o.length){let l=Object.values(o);for(let s of l)if(d(s)&&(s=s.trim(),!v.test(s)))if(super.has(s))c.set(s,!0);else{try{super.add(s)}catch{}super.has(s)&&c.set(s,!0)}}if(Array.isArray(a)&&a.length){let l=Object.values(a);for(let s of l)d(s)&&(s=s.trim(),s&&c.set(s,!1))}}let m;if(super.isURI(e)){let{hash:l,href:s,pathname:i,protocol:E,search:y}=new URL(e),b=E.replace(/:$/,""),S=b.split("+"),U;if(p)U=S.every(f=>c.get(f));else for(let[f,w]of c.entries())if(U=w||b!==f&&S.every(_=>_!==f),!U)break;if(U){let f,w=s;if(S.includes("data")){let[_,...Y]=i.split(","),k=`${Y.join(",")}${y}${l}`,R=_.split(";"),h=k;if(R[R.length-1]==="base64")R.pop(),h=oe(k);else try{let z=q(h),{protocol:C}=new URL(z.trim());C.replace(/:$/,"").split("+").some(u=>v.test(u))&&(w="")}catch{}let M=/data:[^,]*,/.test(h);if(h!==k||M){if(M){let C=[...h.matchAll(W)].reverse();for(let T of C){let[u]=T;G.test(u)&&([u]=G.exec(u)),this.#e++,this.#t.add(u);let P=this.sanitize(u,{allow:["data"]});if(P){let{index:D}=T,[X,J]=[h.substring(0,D),h.substring(D+u.length)];h=`${X}${P}${J}`}}this.#t.has(e)?this.#t.delete(e):f=!0}else this.#t.has(e)?this.#t.delete(e):f=!0;w=`${b}:${R.join(";")},${h}`}else this.#t.has(e)?this.#t.delete(e):f=!0}else f=!0;w?(m=w.replace(Z,I).replace(ie,N),f&&(m=m.replace(ee,N),this.#e=0)):(m=w,this.#e=0)}}return m||null}parse(e,r){if(!d(e))throw new TypeError(`Expected String but got ${g(e)}.`);let o=this.sanitize(e,r??{allow:["data","file"]}),a=new Map([["input",e]]);if(o){let n=new URL(o),{pathname:c,protocol:p}=n,m=p.replace(/:$/,"").split("+");if(a.set("valid",!0),m.includes("data")){let l=new Map,[s,...i]=c.split(","),E=`${i.join(",")}`,y=s.split(";"),b=y[y.length-1]==="base64";b&&y.pop(),l.set("mime",y.join(";")),l.set("base64",b),l.set("data",E),a.set("data",Object.fromEntries(l))}else a.set("data",null);for(let l in n){let s=n[l];typeof s!="function"&&a.set(l,s)}}else a.set("valid",!1);return Object.fromEntries(a)}},$=new L,H=t=>$.isURI(t),ne=async t=>await H(t),F=(t,e)=>{let r=$.parse(t,e??{allow:[],deny:[],only:[]}),o;if(r){let{href:a}=r;o=a}return o??null},ce=async(t,e)=>await F(t,e),B=t=>$.parse(t),pe=async t=>await B(t);export{$ as default,ne as isURI,H as isURISync,pe as parseURL,B as parseURLSync,ce as sanitizeURL,F as sanitizeURLSync}; | ||
//# sourceMappingURL=url-sanitizer.min.js.map |
@@ -6,3 +6,9 @@ /** | ||
export { | ||
default, isURI, isURISync, sanitizeURL, sanitizeURLSync | ||
default, | ||
isURI, | ||
isURISync, | ||
parseURL, | ||
parseURLSync, | ||
sanitizeURL, | ||
sanitizeURLSync | ||
} from './src/mjs/uri-util.js'; |
@@ -32,2 +32,6 @@ { | ||
}, | ||
"dependencies": { | ||
"dompurify": "^2.4.3", | ||
"jsdom": "^21.1.0" | ||
}, | ||
"devDependencies": { | ||
@@ -54,3 +58,3 @@ "@babel/eslint-parser": "^7.19.1", | ||
}, | ||
"version": "0.5.5" | ||
"version": "0.5.7" | ||
} |
111
README.md
@@ -125,14 +125,14 @@ # URL Sanitizer | ||
* `data.data` **[string][1]** Data part of the data URL. | ||
* `href` **[string][1]** Same as URL API. | ||
* `origin` **[string][1]** Same as URL API. | ||
* `protocol` **[string][1]** Same as URL API. | ||
* `username` **[string][1]** Same as URL API. | ||
* `password` **[string][1]** Same as URL API. | ||
* `host` **[string][1]** Same as URL API. | ||
* `hostname` **[string][1]** Same as URL API. | ||
* `port` **[string][1]** Same as URL API. | ||
* `pathname` **[string][1]** Same as URL API. | ||
* `search` **[string][1]** Same as URL API. | ||
* `searchParams` **[object][3]** Same as URL API. | ||
* `hash` **[string][1]** Same as URL API. | ||
* `href` **[string][1]** Sanitized URL input. | ||
* `origin` **[string][1]** Scheme, domain and port of the sanitized URL. | ||
* `protocol` **[string][1]** Protocol scheme of the sanitized URL. | ||
* `username` **[string][1]** Username specified before the domain name. | ||
* `password` **[string][1]** Password specified before the domain name. | ||
* `host` **[string][1]** Domain and port of the sanitized URL. | ||
* `hostname` **[string][1]** Domain of the sanitized URL. | ||
* `port` **[string][1]** Port number of the sanitized URL. | ||
* `pathname` **[string][1]** Path of the sanitized URL. | ||
* `search` **[string][1]** Query string of the sanitized URL. | ||
* `searchParams` **[object][3]** [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object. | ||
* `hash` **[string][1]** Fragment identifier of the sanitized URL. | ||
@@ -241,7 +241,9 @@ ```javascript | ||
Get an array of URI schemes registered at [iana.org](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml "Uniform Resource Identifier (URI) Schemes"). | ||
* Historical schemes omitted. | ||
* `moz-extension` scheme added. | ||
Get a list of registered URI schemes. | ||
Returns **[Array][4]<[string][1]>** Array of registered URI schemes. | ||
* Includes schemes registered at [iana.org](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml "Uniform Resource Identifier (URI) Schemes") by default. | ||
* Historical schemes omitted. | ||
* `moz-extension` scheme added. | ||
* Also includes custom schemes added via [urlSanitizer.add()](#urlsanitizeraddscheme). | ||
@@ -273,3 +275,3 @@ ```javascript | ||
Add a scheme to the list of URI schemes. | ||
Add a scheme to the list of registered URI schemes. | ||
* `javascript` and `vbscript` schemes can not be registered. It throws. | ||
@@ -296,3 +298,3 @@ | ||
Remove a scheme from the list of URI schemes. | ||
Remove a scheme from the list of registered URI schemes. | ||
@@ -322,2 +324,77 @@ #### Parameters | ||
## Reactivate tags and purify DOM | ||
It's outside the scope of URL Sanitizer, but escaped tags in sanitized data URL can be inconvenient. | ||
For example, embedded SVG in a data URL. | ||
For such cases, here is a sample code that reactivate tags and purify the DOM. | ||
* [dompurify](https://www.npmjs.com/package/dompurify) is required. | ||
* If you're using Node.js, [jsdom](https://www.npmjs.com/package/jsdom) is also required. | ||
```shell | ||
npm i dompurify jsdom | ||
``` | ||
```javascript | ||
import DOMPurify from 'dompurify'; | ||
import { JSDOM } from 'jsdom'; | ||
import { parseURL } from 'url-sanitizer'; | ||
const { window } = new JSDOM(''); | ||
const domPurify = DOMPurify(window); | ||
/** | ||
* get purified DOM from a data URL | ||
* | ||
* @param {string} url - URL input | ||
* @returns {?string} - purified DOM, `null` if the given URL is not a data URL | ||
*/ | ||
const getPurifiedDOMFromDataURL = async url => { | ||
const parsedURL = await parseURL(url); | ||
const { data: parsedDataURL } = parsedURL; | ||
let purifiedDOM; | ||
if (parsedDataURL) { | ||
const { base64, data, mime } = parsedDataURL; | ||
if (!base64 && | ||
/^(?:text\/(?:ht|x)ml|application\/(?:xhtml\+)?xml|image\/svg\+xml)/ | ||
.test(mime)) { | ||
let parsedData = data; | ||
const matchedHTMLChars = | ||
parsedData.matchAll(/%26(?:(?:l|g|quo)t|%2339);?/g); | ||
const items = [...matchedHTMLChars].reverse(); | ||
const unescapeURLEncodedHTMLChars = ch => { | ||
let unescapedChar; | ||
if (/%26lt;?/.test(ch)) { | ||
unescapedChar = '<'; | ||
} else if (/%26gt;?/.test(ch)) { | ||
unescapedChar = '>'; | ||
} else if (/%26quot;?/.test(ch)) { | ||
unescapedChar = '"'; | ||
} else if (/%26%2339;?/.test(ch)) { | ||
unescapedChar = "'"; | ||
} else { | ||
unescapedChar = ch; | ||
} | ||
return unescapedChar; | ||
}; | ||
for (const item of items) { | ||
const [htmlChar] = item; | ||
const { index } = item; | ||
const [preHTMLChar, postHTMLChar] = [ | ||
parsedData.substring(0, index), | ||
parsedData.substring(index + htmlChar.length) | ||
]; | ||
const unescapedHTMLChar = unescapeURLEncodedHTMLChars(htmlChar); | ||
parsedData = `${preHTMLChar}${unescapedHTMLChar}${postHTMLChar}`; | ||
} | ||
purifiedDOM = domPurify.sanitize(decodeURIComponent(parsedData)); | ||
} | ||
} | ||
return purifiedDOM ?? null; | ||
}; | ||
``` | ||
--- | ||
## Acknowledgments | ||
@@ -324,0 +401,0 @@ |
@@ -448,14 +448,14 @@ /** | ||
* @property {string} data.data - data part of the data URL | ||
* @property {string} href - same as URL API | ||
* @property {string} origin - same as URL API | ||
* @property {string} protocol - same as URL API | ||
* @property {string} username - same as URL API | ||
* @property {string} password - same as URL API | ||
* @property {string} host - same as URL API | ||
* @property {string} hostname - same as URL API | ||
* @property {string} port - same as URL API | ||
* @property {string} pathname - same as URL API | ||
* @property {string} search - same as URL API | ||
* @property {object} searchParams - same as URL API | ||
* @property {string} hash - same as URL API | ||
* @property {string} href - sanitized URL input | ||
* @property {string} origin - scheme, domain and port of the sanitized URL | ||
* @property {string} protocol - protocol scheme of the sanitized URL | ||
* @property {string} username - username specified before the domain name | ||
* @property {string} password - password specified before the domain name | ||
* @property {string} host - domain and port of the sanitized URL | ||
* @property {string} hostname - domain of the sanitized URL | ||
* @property {string} port - port number of the sanitized URL | ||
* @property {string} pathname - path of the sanitized URL | ||
* @property {string} search - query string of the sanitized URL | ||
* @property {object} searchParams - URLSearchParams object | ||
* @property {string} hash - fragment identifier of the sanitized URL | ||
*/ | ||
@@ -467,9 +467,10 @@ | ||
* @param {string} url - URL input | ||
* @param {object} opt - options | ||
* @returns {ParsedURL} - result with extended props based on URL API | ||
*/ | ||
parse(url) { | ||
parse(url, opt) { | ||
if (!isString(url)) { | ||
throw new TypeError(`Expected String but got ${getType(url)}.`); | ||
} | ||
const sanitizedUrl = this.sanitize(url, { | ||
const sanitizedUrl = this.sanitize(url, opt ?? { | ||
allow: ['data', 'file'] | ||
@@ -544,7 +545,15 @@ }); | ||
*/ | ||
const sanitizeUrl = (url, opt) => urlSanitizer.sanitize(url, opt ?? { | ||
allow: [], | ||
deny: [], | ||
only: [] | ||
}); | ||
const sanitizeUrl = (url, opt) => { | ||
const parsedUrl = urlSanitizer.parse(url, opt ?? { | ||
allow: [], | ||
deny: [], | ||
only: [] | ||
}); | ||
let res; | ||
if (parsedUrl) { | ||
const { href } = parsedUrl; | ||
res = href; | ||
} | ||
return res ?? null; | ||
}; | ||
@@ -551,0 +560,0 @@ /** |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
151231
2295
414
2
+ Addeddompurify@^2.4.3
+ Addedjsdom@^21.1.0
+ Added@tootallnate/once@2.0.0(transitive)
+ Addedabab@2.0.6(transitive)
+ Addedacorn@8.14.0(transitive)
+ Addedacorn-globals@7.0.1(transitive)
+ Addedacorn-walk@8.3.4(transitive)
+ Addedagent-base@6.0.2(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedcall-bind-apply-helpers@1.0.2(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcssstyle@3.0.0(transitive)
+ Addeddata-urls@4.0.0(transitive)
+ Addeddebug@4.4.0(transitive)
+ Addeddecimal.js@10.5.0(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addeddomexception@4.0.0(transitive)
+ Addeddompurify@2.5.8(transitive)
+ Addeddunder-proto@1.0.1(transitive)
+ Addedentities@4.5.0(transitive)
+ Addedes-define-property@1.0.1(transitive)
+ Addedes-errors@1.3.0(transitive)
+ Addedes-object-atoms@1.1.1(transitive)
+ Addedes-set-tostringtag@2.1.0(transitive)
+ Addedescodegen@2.1.0(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedestraverse@5.3.0(transitive)
+ Addedesutils@2.0.3(transitive)
+ Addedform-data@4.0.2(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedget-intrinsic@1.2.7(transitive)
+ Addedget-proto@1.0.1(transitive)
+ Addedgopd@1.2.0(transitive)
+ Addedhas-symbols@1.1.0(transitive)
+ Addedhas-tostringtag@1.0.2(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedhtml-encoding-sniffer@3.0.0(transitive)
+ Addedhttp-proxy-agent@5.0.0(transitive)
+ Addedhttps-proxy-agent@5.0.1(transitive)
+ Addediconv-lite@0.6.3(transitive)
+ Addedis-potential-custom-element-name@1.0.1(transitive)
+ Addedjsdom@21.1.2(transitive)
+ Addedmath-intrinsics@1.1.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedms@2.1.3(transitive)
+ Addednwsapi@2.2.16(transitive)
+ Addedparse5@7.2.1(transitive)
+ Addedpsl@1.15.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedquerystringify@2.2.0(transitive)
+ Addedrequires-port@1.0.0(transitive)
+ Addedrrweb-cssom@0.6.0(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsaxes@6.0.0(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addedsymbol-tree@3.2.4(transitive)
+ Addedtough-cookie@4.1.4(transitive)
+ Addedtr46@4.1.1(transitive)
+ Addeduniversalify@0.2.0(transitive)
+ Addedurl-parse@1.5.10(transitive)
+ Addedw3c-xmlserializer@4.0.0(transitive)
+ Addedwebidl-conversions@7.0.0(transitive)
+ Addedwhatwg-encoding@2.0.0(transitive)
+ Addedwhatwg-mimetype@3.0.0(transitive)
+ Addedwhatwg-url@12.0.1(transitive)
+ Addedws@8.18.0(transitive)
+ Addedxml-name-validator@4.0.0(transitive)
+ Addedxmlchars@2.2.0(transitive)