url-sanitizer
Advanced tools
Comparing version 0.2.3 to 0.3.0
@@ -722,3 +722,12 @@ // src/mjs/common.js | ||
var URLSanitizer = class extends URISchemes { | ||
/* private fields */ | ||
#recurse; | ||
/** | ||
* construct | ||
*/ | ||
constructor() { | ||
super(); | ||
this.#recurse = /* @__PURE__ */ new Map(); | ||
} | ||
/** | ||
* sanitize URL | ||
@@ -732,9 +741,8 @@ * NOTE: `data` and/or `file` schemes must be explicitly allowed | ||
* @param {Array.<string>} opt.deny - array of denied schemes | ||
* @param {boolean} opt.escapeTags - escape tags and quotes in data URL | ||
* @returns {?string} - sanitized URL | ||
*/ | ||
sanitize(url, opt = { allow: [], deny: [], escapeTags: true }) { | ||
sanitize(url, opt = { allow: [], deny: [] }) { | ||
let sanitizedUrl; | ||
if (super.isURI(url)) { | ||
const { allow, deny, escapeTags } = opt ?? {}; | ||
const { allow, deny } = opt ?? {}; | ||
const { href, pathname, protocol } = new URL(url); | ||
@@ -789,14 +797,19 @@ const scheme = protocol.replace(/:$/, ""); | ||
if (parsedData !== data) { | ||
const regDataUrl = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/g; | ||
if (regDataUrl.test(parsedData)) { | ||
const dataUrlArr = []; | ||
let arr = regDataUrl.exec(parsedData); | ||
do { | ||
if (arr) { | ||
dataUrlArr.push(arr); | ||
} | ||
} while (arr = regDataUrl.exec(parsedData)); | ||
if (dataUrlArr.length) { | ||
for (const i of dataUrlArr) { | ||
const [dataUrl] = i; | ||
if (/data:[^,]*,/.test(parsedData)) { | ||
const regDataUrl = /data:[^,]*,[^"]+/g; | ||
const regBase64DataUrl = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/; | ||
const matchedDataUrls = parsedData.matchAll(regDataUrl); | ||
const items = [...matchedDataUrls].reverse(); | ||
if (items.length) { | ||
for (const item of items) { | ||
const { index } = item; | ||
let [dataUrl] = item; | ||
if (regBase64DataUrl.test(dataUrl)) { | ||
[dataUrl] = regBase64DataUrl.exec(dataUrl); | ||
} | ||
const [beforeDataUrl, afterDataUrl] = [ | ||
parsedData.substring(0, index), | ||
parsedData.substring(index + dataUrl.length) | ||
]; | ||
this.#recurse.set(dataUrl, dataUrl); | ||
const parsedDataUrl = this.sanitize(dataUrl, { | ||
@@ -806,33 +819,24 @@ allow: ["data"] | ||
if (parsedDataUrl) { | ||
parsedData = parsedData.replace(dataUrl, parsedDataUrl); | ||
parsedData = [ | ||
beforeDataUrl, | ||
parsedDataUrl, | ||
afterDataUrl | ||
].join(""); | ||
} | ||
} | ||
type = 0; | ||
} | ||
} else if (/data:[^,]*,/.test(parsedData) && !(escapeTags ?? true)) { | ||
const dataArr = parsedData.split(/data:[^,]*,/); | ||
const l = dataArr.length; | ||
let i = 1; | ||
while (i < l) { | ||
const dataItem = dataArr[i].replace(regEncodedChars, escapeUrlEncodedHtmlChars); | ||
parsedData = parsedData.replace(dataArr[i], dataItem); | ||
i++; | ||
} | ||
type = 0; | ||
} | ||
urlToSanitize = `${scheme}:${mediaType.join(";")},${parsedData}`; | ||
if (escapeTags ?? true) { | ||
if (this.#recurse.has(url)) { | ||
this.#recurse.delete(url); | ||
} else { | ||
type = 1; | ||
} else if (!Number.isInteger(type)) { | ||
type = 2; | ||
} | ||
} else if (escapeTags ?? true) { | ||
urlToSanitize = `${scheme}:${mediaType.join(";")},${parsedData}`; | ||
} else { | ||
type = 1; | ||
} else { | ||
type = 2; | ||
} | ||
} else if (escapeTags ?? true) { | ||
} else if (this.#recurse.has(url)) { | ||
this.#recurse.delete(url); | ||
} else { | ||
type = 1; | ||
} else { | ||
type = 2; | ||
} | ||
@@ -842,12 +846,6 @@ } else { | ||
} | ||
switch (type) { | ||
case 1: | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString).replace(regAmp, escapeUrlEncodedHtmlChars).replace(regEncodedChars, escapeUrlEncodedHtmlChars); | ||
break; | ||
case 2: | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString).replace(regAmp, escapeUrlEncodedHtmlChars); | ||
break; | ||
case 0: | ||
default: | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString); | ||
if (type === 1) { | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString).replace(regAmp, escapeUrlEncodedHtmlChars).replace(regEncodedChars, escapeUrlEncodedHtmlChars); | ||
} else { | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString).replace(regAmp, escapeUrlEncodedHtmlChars); | ||
} | ||
@@ -867,4 +865,3 @@ } | ||
allow: [], | ||
deny: [], | ||
escapeTags: true | ||
deny: [] | ||
}); | ||
@@ -871,0 +868,0 @@ var sanitizeURL = async (url, opt) => { |
@@ -1,2 +0,2 @@ | ||
var w=e=>Object.prototype.toString.call(e).slice(8,-1),d=e=>typeof e=="string"||e instanceof String;var L=[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 P=["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 Z=16,u=e=>{if(!d(e))throw new TypeError(`Expected String but got ${w(e)}.`);let s=[];for(let r of e)s.push(`%${r.charCodeAt(0).toString(Z).toUpperCase()}`);return s.join("")},v=e=>{if(d(e))if(/^%[\dA-F]{2}$/i.test(e))e=e.toUpperCase();else throw new Error(`${e} is not a URL encoded character.`);else throw new TypeError(`Expected String but got ${w(e)}.`);let[s,r,i,a,o,l]=["&","#","<",">",'"',"'"].map(u),c;return e===s?c=`${s}amp;`:e===i?c=`${s}lt;`:e===a?c=`${s}gt;`:e===o?c=`${s}quot;`:e===l?c=`${s}${r}39;`:c=e,c},G=e=>{if(!d(e))throw new TypeError(`Expected String but got ${w(e)}.`);let s=atob(e),r=Uint8Array.from([...s].map(o=>o.charCodeAt(0))),i=new Set(L),a;return r.every(o=>i.has(o))?a=s.replace(/\s/g,u):a=e,a},T=class{#e;constructor(){this.#e=new Set(P)}get(){return[...this.#e]}has(s){return this.#e.has(s)}add(s){if(d(s)){if(/(?:java|vb)script/.test(s)||!/^[a-z][a-z0-9+\-.]*$/.test(s))throw new Error(`Invalid scheme: ${s}`)}else throw new TypeError(`Expected String but got ${w(s)}.`);return this.#e.add(s),[...this.#e]}remove(s){return this.#e.delete(s)}isURI(s){let r;if(d(s))try{let{protocol:i}=new URL(s),a=i.replace(/:$/,""),o=a.split("+");r=/^(?:ext|web)\+[a-z]+$/.test(a)||o.every(l=>this.#e.has(l))}catch{r=!1}return!!r}},S=class extends T{sanitize(s,r={allow:[],deny:[],escapeTags:!0}){let i;if(super.isURI(s)){let{allow:a,deny:o,escapeTags:l}=r??{},{href:c,pathname:M,protocol:Y}=new URL(s),k=Y.replace(/:$/,""),j=k.split("+"),y=new Map([["data",!1],["file",!1]]);if(Array.isArray(a)&&a.length){let m=Object.values(a);for(let t of m)d(t)&&(t=t.trim(),/(?:java|vb)script/.test(t)?y.set(t,!1):t&&y.set(t,!0))}if(Array.isArray(o)&&o.length){let m=Object.values(o);for(let t of m)d(t)&&(t=t.trim(),t&&y.set(t,!1))}let $;for(let[m,t]of y.entries())if($=t||k!==m&&j.every(U=>U!==m),!$)break;if($){let[m,t,U,B,F]=["&","<",">",'"',"'"].map(u),z=/[<>"']/g,A=new RegExp(m,"g"),C=new RegExp(`(${t}|${U}|${B}|${F})`,"g"),n,b=c;if(j.includes("data")){let[H,I]=M.split(","),q=H.split(";");if(q.pop()==="base64"){let p=G(I);if(p!==I){let E=/data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/g;if(E.test(p)){let f=[],h=E.exec(p);do h&&f.push(h);while(h=E.exec(p));if(f.length){for(let g of f){let[x]=g,O=this.sanitize(x,{allow:["data"]});O&&(p=p.replace(x,O))}n=0}}else if(/data:[^,]*,/.test(p)&&!(l??!0)){let f=p.split(/data:[^,]*,/),h=f.length,g=1;for(;g<h;){let x=f[g].replace(C,v);p=p.replace(f[g],x),g++}n=0}b=`${k}:${q.join(";")},${p}`,l??!0?n=1:Number.isInteger(n)||(n=2)}else l??!0?n=1:n=2}else l??!0?n=1:n=2}else n=1;switch(n){case 1:i=b.replace(z,u).replace(A,v).replace(C,v);break;case 2:i=b.replace(z,u).replace(A,v);break;case 0:default:i=b.replace(z,u)}}}return i||null}},R=new S,_=e=>R.isURI(e),J=async e=>await _(e),D=(e,s)=>R.sanitize(e,s??{allow:[],deny:[],escapeTags:!0}),K=async(e,s)=>await D(e,s);export{R as default,J as isURI,_ as isURISync,K as sanitizeURL,D as sanitizeURLSync}; | ||
var g=s=>Object.prototype.toString.call(s).slice(8,-1),c=s=>typeof s=="string"||s instanceof String;var I=[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 K=16,h=s=>{if(!c(s))throw new TypeError(`Expected String but got ${g(s)}.`);let e=[];for(let a of s)e.push(`%${a.charCodeAt(0).toString(K).toUpperCase()}`);return e.join("")},v=s=>{if(c(s))if(/^%[\dA-F]{2}$/i.test(s))s=s.toUpperCase();else throw new Error(`${s} is not a URL encoded character.`);else throw new TypeError(`Expected String but got ${g(s)}.`);let[e,a,o,r,i,d]=["&","#","<",">",'"',"'"].map(h),n;return s===e?n=`${e}amp;`:s===o?n=`${e}lt;`:s===r?n=`${e}gt;`:s===i?n=`${e}quot;`:s===d?n=`${e}${a}39;`:n=s,n},N=s=>{if(!c(s))throw new TypeError(`Expected String but got ${g(s)}.`);let e=atob(s),a=Uint8Array.from([...e].map(i=>i.charCodeAt(0))),o=new Set(I),r;return a.every(i=>o.has(i))?r=e.replace(/\s/g,h):r=s,r},U=class{#e;constructor(){this.#e=new Set(O)}get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(c(e)){if(/(?:java|vb)script/.test(e)||!/^[a-z][a-z0-9+\-.]*$/.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 a;if(c(e))try{let{protocol:o}=new URL(e),r=o.replace(/:$/,""),i=r.split("+");a=/^(?:ext|web)\+[a-z]+$/.test(r)||i.every(d=>this.#e.has(d))}catch{a=!1}return!!a}},k=class extends U{#e;constructor(){super(),this.#e=new Map}sanitize(e,a={allow:[],deny:[]}){let o;if(super.isURI(e)){let{allow:r,deny:i}=a??{},{href:d,pathname:n,protocol:M}=new URL(e),w=M.replace(/:$/,""),z=w.split("+"),u=new Map([["data",!1],["file",!1]]);if(Array.isArray(r)&&r.length){let p=Object.values(r);for(let t of p)c(t)&&(t=t.trim(),/(?:java|vb)script/.test(t)?u.set(t,!1):t&&u.set(t,!0))}if(Array.isArray(i)&&i.length){let p=Object.values(i);for(let t of p)c(t)&&(t=t.trim(),t&&u.set(t,!1))}let y;for(let[p,t]of u.entries())if(y=t||w!==p&&z.every(b=>b!==p),!y)break;if(y){let[p,t,b,_,B]=["&","<",">",'"',"'"].map(h),E=/[<>"']/g,S=new RegExp(p,"g"),Y=new RegExp(`(${t}|${b}|${_}|${B})`,"g"),f,x=d;if(z.includes("data")){let[F,R]=n.split(","),j=F.split(";");if(j.pop()==="base64"){let m=N(R);if(m!==R){if(/data:[^,]*,/.test(m)){let H=/data:[^,]*,[^"]+/g,T=/data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/,A=[...m.matchAll(H)].reverse();if(A.length)for(let C of A){let{index:D}=C,[l]=C;T.test(l)&&([l]=T.exec(l));let[X,Z]=[m.substring(0,D),m.substring(D+l.length)];this.#e.set(l,l);let q=this.sanitize(l,{allow:["data"]});q&&(m=[X,q,Z].join(""))}}this.#e.has(e)?this.#e.delete(e):f=1,x=`${w}:${j.join(";")},${m}`}else f=1}else this.#e.has(e)?this.#e.delete(e):f=1}else f=1;f===1?o=x.replace(E,h).replace(S,v).replace(Y,v):o=x.replace(E,h).replace(S,v)}}return o||null}},$=new k,L=s=>$.isURI(s),Q=async s=>await L(s),P=(s,e)=>$.sanitize(s,e??{allow:[],deny:[]}),V=async(s,e)=>await P(s,e);export{$ as default,Q as isURI,L as isURISync,V as sanitizeURL,P as sanitizeURLSync}; | ||
//# sourceMappingURL=url-sanitizer.min.js.map |
@@ -51,5 +51,5 @@ { | ||
"sinon": "^15.0.1", | ||
"undici": "^5.15.2" | ||
"undici": "^5.16.0" | ||
}, | ||
"version": "0.2.3" | ||
"version": "0.3.0" | ||
} |
@@ -42,3 +42,2 @@ # urlSanitizer | ||
* `opt.deny` **[Array][4]<[string][1]>** array of denied schemes | ||
* `opt.escapeTags` **[boolean][2]** escape tags and quotes in data URL | ||
@@ -63,17 +62,2 @@ Returns **[Promise][5]<[string][1]?>** sanitized URL, `null`able | ||
// -> 'data:text/html,<script>alert(1);</script>' | ||
// Also an option if you don't want to escape tags and quotes in data URL | ||
// But use it with care | ||
const res4 = await sanitizeURL('data:image/svg+xml,%3Csvg%3E%3C/svg%3E', { | ||
allow: ['data'], | ||
escapeTags: false | ||
}).then(res => decodeURIComponent(res)); | ||
// -> 'data:image/svg+xml,<svg></svg>' | ||
const res5 = await sanitizeURL('data:text/html,%3Cscript%3Ealert(1);%3C/script%3E', { | ||
allow: ['data'], | ||
escapeTags: false | ||
}).then(res => decodeURIComponent(res)); | ||
// WATCH OUT!!! | ||
// -> 'data:text/html,<script>alert(1);</script>' | ||
``` | ||
@@ -80,0 +64,0 @@ |
@@ -177,3 +177,14 @@ /** | ||
export class URLSanitizer extends URISchemes { | ||
/* private fields */ | ||
#recurse; | ||
/** | ||
* construct | ||
*/ | ||
constructor() { | ||
super(); | ||
this.#recurse = new Map(); | ||
} | ||
/** | ||
* sanitize URL | ||
@@ -187,9 +198,8 @@ * NOTE: `data` and/or `file` schemes must be explicitly allowed | ||
* @param {Array.<string>} opt.deny - array of denied schemes | ||
* @param {boolean} opt.escapeTags - escape tags and quotes in data URL | ||
* @returns {?string} - sanitized URL | ||
*/ | ||
sanitize(url, opt = { allow: [], deny: [], escapeTags: true }) { | ||
sanitize(url, opt = { allow: [], deny: [] }) { | ||
let sanitizedUrl; | ||
if (super.isURI(url)) { | ||
const { allow, deny, escapeTags } = opt ?? {}; | ||
const { allow, deny } = opt ?? {}; | ||
const { href, pathname, protocol } = new URL(url); | ||
@@ -246,14 +256,19 @@ const scheme = protocol.replace(/:$/, ''); | ||
if (parsedData !== data) { | ||
const regDataUrl = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/g; | ||
if (regDataUrl.test(parsedData)) { | ||
const dataUrlArr = []; | ||
let arr = regDataUrl.exec(parsedData); | ||
do { | ||
if (arr) { | ||
dataUrlArr.push(arr); | ||
} | ||
} while ((arr = regDataUrl.exec(parsedData))); | ||
if (dataUrlArr.length) { | ||
for (const i of dataUrlArr) { | ||
const [dataUrl] = i; | ||
if (/data:[^,]*,/.test(parsedData)) { | ||
const regDataUrl = /data:[^,]*,[^"]+/g; | ||
const regBase64DataUrl = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/; | ||
const matchedDataUrls = parsedData.matchAll(regDataUrl); | ||
const items = [...matchedDataUrls].reverse(); | ||
if (items.length) { | ||
for (const item of items) { | ||
const { index } = item; | ||
let [dataUrl] = item; | ||
if (regBase64DataUrl.test(dataUrl)) { | ||
[dataUrl] = regBase64DataUrl.exec(dataUrl); | ||
} | ||
const [beforeDataUrl, afterDataUrl] = [ | ||
parsedData.substring(0, index), | ||
parsedData.substring(index + dataUrl.length) | ||
]; | ||
this.#recurse.set(dataUrl, dataUrl); | ||
const parsedDataUrl = this.sanitize(dataUrl, { | ||
@@ -263,35 +278,24 @@ allow: ['data'] | ||
if (parsedDataUrl) { | ||
parsedData = parsedData.replace(dataUrl, parsedDataUrl); | ||
parsedData = [ | ||
beforeDataUrl, | ||
parsedDataUrl, | ||
afterDataUrl | ||
].join(''); | ||
} | ||
} | ||
type = 0; | ||
} | ||
} else if (/data:[^,]*,/.test(parsedData) && | ||
!(escapeTags ?? true)) { | ||
const dataArr = parsedData.split(/data:[^,]*,/); | ||
const l = dataArr.length; | ||
let i = 1; | ||
while (i < l) { | ||
const dataItem = dataArr[i] | ||
.replace(regEncodedChars, escapeUrlEncodedHtmlChars); | ||
parsedData = parsedData.replace(dataArr[i], dataItem); | ||
i++; | ||
} | ||
type = 0; | ||
} | ||
urlToSanitize = `${scheme}:${mediaType.join(';')},${parsedData}`; | ||
if ((escapeTags ?? true)) { | ||
if (this.#recurse.has(url)) { | ||
this.#recurse.delete(url); | ||
} else { | ||
type = 1; | ||
} else if (!Number.isInteger(type)) { | ||
type = 2; | ||
} | ||
} else if ((escapeTags ?? true)) { | ||
urlToSanitize = `${scheme}:${mediaType.join(';')},${parsedData}`; | ||
} else { | ||
type = 1; | ||
} else { | ||
type = 2; | ||
} | ||
} else if ((escapeTags ?? true)) { | ||
} else if (this.#recurse.has(url)) { | ||
this.#recurse.delete(url); | ||
} else { | ||
type = 1; | ||
} else { | ||
type = 2; | ||
} | ||
@@ -301,15 +305,9 @@ } else { | ||
} | ||
switch (type) { | ||
case 1: | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString) | ||
.replace(regAmp, escapeUrlEncodedHtmlChars) | ||
.replace(regEncodedChars, escapeUrlEncodedHtmlChars); | ||
break; | ||
case 2: | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString) | ||
.replace(regAmp, escapeUrlEncodedHtmlChars); | ||
break; | ||
case 0: | ||
default: | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString); | ||
if (type === 1) { | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString) | ||
.replace(regAmp, escapeUrlEncodedHtmlChars) | ||
.replace(regEncodedChars, escapeUrlEncodedHtmlChars); | ||
} else { | ||
sanitizedUrl = urlToSanitize.replace(regChars, getUrlEncodedString) | ||
.replace(regAmp, escapeUrlEncodedHtmlChars); | ||
} | ||
@@ -353,4 +351,3 @@ } | ||
allow: [], | ||
deny: [], | ||
escapeTags: true | ||
deny: [] | ||
}); | ||
@@ -363,3 +360,3 @@ | ||
* @param {object} opt - options | ||
* @returns {Promise<?string>} - sanitized URL | ||
* @returns {Promise.<?string>} - sanitized URL | ||
*/ | ||
@@ -366,0 +363,0 @@ export const sanitizeURL = async (url, opt) => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
103933
1852
185