UNPKG

vite-plugin-csp-guard

Version:

A Vite plugin that lets SPA applications generate a Content Security Policy (CSP).

3 lines (2 loc) 17.1 kB
import{definePolicy as e,self as t,data as s,unsafeEval as r,unsafeInline as n,policyToString as o,mergePolicies as i}from"csp-toolkit";import c from"crypto";import{createFilter as l}from"vite";import*as a from"cheerio";import*as u from"fs";import*as p from"path";const h=e({defaultSrc:[t],imgSrc:[t,s],scriptSrcElem:[t],styleSrcElem:[t]}),d=e({defaultSrc:[t],imgSrc:[t],scriptSrcElem:[t],styleSrcElem:[t]}),f=["tailwind","sass","less","stylus","vue"],m=["vue-router"],y=({source:e,currentPolicy:t})=>{const s=(e=>{try{const t=new URL(e);return`${t.protocol}//${t.hostname}`}catch(e){return!1}})(e);return!!s&&t.includes(s)},g=l("**.css"),v=l(["**.scss","**.less","**.styl"]),_=l(["**/*.js?(*)","**/*.jsx?(*)"]),b=l(["**/*.ts","**/*.tsx"]);l("**.html");function k(e){return"'"+e+"'"}const w=new Set([k(r)]);function $(e){return e?.includes(k(n))??!1}const S=(e,t)=>{const s=c.createHash(t);return s.update(e),s.digest("base64")},E=({hash:e,key:t,data:s,collection:r})=>{if(e.length){const n=r[t],o=`${s.algorithm}-${e}`;n.has(o)||n.set(o,{...s})}},C=e=>{const t={"script-src":!1,"script-src-attr":!1,"script-src-elem":!1,"style-src":!1,"style-src-attr":!1,"style-src-elem":!1};if(!e)return t;return Object.keys(t).forEach((s=>{const n=e[s];var o;($(n)||(o=n,o?.includes(k(r))))&&(t[s]=!0)})),t};function L({html:e,algorithm:t,collection:s,policy:r,bundleContext:n}){const o=a.load(e);return o("script").each((function(e,i){if(Object.keys(i.attribs).length&&i.attribs?.src?.length)try{const e=i.attribs.src;if((({currentPolicy:e,source:t,sourceType:s="script-src",context:r})=>{(e=>!(!e.includes("http://")&&!e.includes("https://")))(t)&&!y({source:t,currentPolicy:e})&&(r?r.warn({message:`${t} is not in the current CSP policy`,pluginCode:"SPECIAL_CODE"}):console.warn(`${t} is not in the current CSP policy`))})({source:e,currentPolicy:r["script-src"]??[],sourceType:"script-src"}),n){const s=!!(c=e).startsWith("/")&&c.slice(1);if(s){const e=n[s];e&&o(i).attr("integrity",`${t}-${e.hash}`)}}}catch(e){console.error("Error hashing script src",e)}var c;if("text"===i.childNodes?.[0]?.type){const e=o.text([i.childNodes?.[0]]);if(e.length){const r=S(e,t);E({hash:r,key:"script-src-elem",data:{algorithm:t,content:e},collection:s})}}})),{HASH_COLLECTION:s,html:o.html()}}const j={"script-src-elem":"script-src","style-src-elem":"style-src"},I=e=>e.replace(/__VITE_PRELOAD__/g,"[]"),x=(e,t)=>{let s=0,r=e.replace(/__VITE_PRELOAD__/g,(()=>{if(0===s)return s++,"void 0";const e=2*(s-1);return s++,`__vite__mapDeps([${e},${e+1}])`}));if(!r.includes("const __vite__mapDeps=")){const e=(e=>{const t=Object.values(e).find((e=>"chunk"===e.type&&e.fileName.includes("index")&&e.dynamicImports.length>0));if(!t)return'const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["placeholder.js"])))=>i.map(i=>d[i]);\n';const s=[],r=new Set;for(const n of t.dynamicImports){const t=Object.values(e).find((e=>"chunk"===e.type&&e.fileName===n));if(!t)continue;const o=`"assets/${t.fileName.split("/").pop()}"`;if(r.has(o)||(s.push(o),r.add(o)),t.viteMetadata?.importedCss){const e=Array.isArray(t.viteMetadata.importedCss)?t.viteMetadata.importedCss:t.viteMetadata.importedCss instanceof Set?Array.from(t.viteMetadata.importedCss):[];for(const t of e){const e=`"assets/${t.split("/").pop()}"`;r.has(e)||(s.push(e),r.add(e))}}}return 0===s.length&&s.push('"placeholder.js"'),`const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[${s.join(",")}])))=>i.map(i=>d[i]);\n`})(t);if(e.includes('"placeholder.js"')&&s<=1)return r;r=e+r}return r},P=(e,t)=>{try{u.writeFileSync(p.resolve(process.cwd(),t),e),console.log(`Debug file written: ${t}`)}catch(e){console.error("Error writing debug file:",e)}},A=({html:e,context:{server:t,bundle:s,chunk:r,path:n,filename:i},pluginContext:c,isTransformationStatusEmpty:l,cspContext:u,sri:p,chunkHashes:h,transformPolicy:d})=>{const{algorithm:f,policy:m,collection:y,shouldSkip:g,debug:v,requirements:_}=u;if(l&&t)return;const b={};if(s&&p)for(const e of Object.keys(s)){const t=s[e];if(t&&"chunk"===t.type&&!g["script-src-elem"]){let r;if(h&&h.has(e))r=h.get(e),v&&console.log(`Using pre-calculated hash for ${e}: sha256-${r.substring(0,20)}...`);else{let n=t.code;n.includes("__VITE_PRELOAD__")&&(v&&P(n,`temp-original-${e.replace(/\//g,"-")}.js`),n=_.strongLazyLoading?x(n,s):I(n),v&&P(n,`temp-transformed-${e.replace(/\//g,"-")}.js`)),t.code=n,r=S(n,f),v&&console.log(`Fallback hash calculation for ${e}: sha256-${r.substring(0,20)}...`)}y["script-src-elem"].has(`${f}-${r}`)||E({hash:r,key:"script-src-elem",data:{algorithm:f,content:""},collection:y}),b[e]={type:"chunk",hash:r}}}const{html:k,HASH_COLLECTION:C}=L({html:e,algorithm:f,collection:y,policy:m,bundleContext:s?b:void 0}),A=(({collection:e,policy:t})=>{const s={...t};for(const[t,r]of Object.entries(e)){const e=r,n=s[t]??[];if(!$(n)&&e.size>0){const r=j[t],o=(r?s[r]??[]:[]).filter((e=>!w.has(e)&&!n.includes(e)));s[t]=[...n,...o,...Array.from(e.keys())]}}return o(s)})({collection:C,policy:m});let O=A;if(d){const e=d(A,{command:u.isDevMode?"serve":"build",algorithm:u.algorithm});O=void 0===e?A:e}const R=null!=O?((e,t)=>{const s=a.load(e),r=`<meta http-equiv="Content-Security-Policy" content="${t}">`;return s("head").prepend(r),s.html()})(k,O):k;return{html:R,tags:[]}};class O{constructor(){this.should_skip=!1,this.should_remove=!1,this.replacement=null,this.context={skip:()=>this.should_skip=!0,remove:()=>this.should_remove=!0,replace:e=>this.replacement=e}}replace(e,t,s,r){e&&t&&(null!=s?e[t][s]=r:e[t]=r)}remove(e,t,s){e&&t&&(null!=s?e[t].splice(s,1):delete e[t])}}class R extends O{constructor(e,t){super(),this.should_skip=!1,this.should_remove=!1,this.replacement=null,this.context={skip:()=>this.should_skip=!0,remove:()=>this.should_remove=!0,replace:e=>this.replacement=e},this.enter=e,this.leave=t}visit(e,t,s,r){if(e){if(this.enter){const n=this.should_skip,o=this.should_remove,i=this.replacement;this.should_skip=!1,this.should_remove=!1,this.replacement=null,this.enter.call(this.context,e,t,s,r),this.replacement&&(e=this.replacement,this.replace(t,s,r,e)),this.should_remove&&this.remove(t,s,r);const c=this.should_skip,l=this.should_remove;if(this.should_skip=n,this.should_remove=o,this.replacement=i,c)return e;if(l)return null}let n;for(n in e){const t=e[n];if(t&&"object"==typeof t)if(Array.isArray(t)){const s=t;for(let t=0;t<s.length;t+=1){const r=s[t];M(r)&&(this.visit(r,e,n,t)||t--)}}else M(t)&&this.visit(t,e,n,null)}if(this.leave){const n=this.replacement,o=this.should_remove;this.replacement=null,this.should_remove=!1,this.leave.call(this.context,e,t,s,r),this.replacement&&(e=this.replacement,this.replace(t,s,r,e)),this.should_remove&&this.remove(t,s,r);const i=this.should_remove;if(this.replacement=n,this.should_remove=o,i)return null}}return e}}function M(e){return null!==e&&"object"==typeof e&&"type"in e&&"string"==typeof e.type}const N=(e,t)=>{if(!e)return!1;return function(e,{enter:t,leave:s}){new R(t,s).visit(e,null)}(e,{enter(e){if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.property.type&&"insertRule"===e.callee.property.name&&e.arguments.length>0){const t=e.arguments[0];"Identifier"===t?.type?console.log("Inserting rule with identifier:",t.name):"Literal"===t?.type&&console.log("Inserting rule with literal content:",t.value)}}}),!1},D={mpa:!1,cssInJs:!1},T=/\.(css|js|mjs)($|\?)/i;function H(e,t,s){const r={};for(const[n,o]of Object.entries(e)){if(!T.test(n))continue;let e;if("asset"===o.type){const t=o;if(!t.source)continue;e="string"==typeof t.source?t.source:Buffer.from(t.source).toString()}else{if("chunk"!==o.type)continue;{const t=o;if(!t.code)continue;e=t.code}}try{const o=`${t}-${S(e,t)}`;r[p.posix.join("/",n)]=o,s&&console.log(`[SRI] Hashed ${n}: ${o.substring(0,30)}...`)}catch(e){s&&console.warn(`[SRI] Failed to hash ${n}:`,e)}}return r}function V(e,t){if(e)try{return t[new URL(e,"http://localhost").pathname]}catch{return t[e]||t[`/${e}`]}}function z(e,t){if(!e||!t)return!1;if(e===t)return!0;const s=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${s}$`).test(t)}function B(e,t){if(!t.length)return!1;const s=e.attribs||{},r=s.id,n=s.src,o=s.href;for(const e of t)if(r&&z(e,r)||n&&z(e,n)||o&&z(e,o))return!0;return!1}function F(e,t,s,r,n,o=[],i=!1,c){const l=a.load(e);c?.length&&l('meta[http-equiv="Content-Security-Policy"]').each(((e,t)=>{const s=l(t).attr("content");if(!s)return;let r=s;for(const{old:e,new:t}of c){const s=`'${e}'`,n=`'${t}'`;r.includes(s)&&(r=r.replace(s,n))}l(t).attr("content",r)})),l("script[src]").each(((e,s)=>{if(B(s,o))return;const r=l(s).attr("src"),i=r?V(r,t):void 0;i&&(l(s).attr("integrity",i),n&&l(s).attr("crossorigin",n))})),l("link[href]").each(((e,s)=>{if(B(s,o))return;const r=l(s).attr("rel")?.toLowerCase(),i=l(s).attr("as")?.toLowerCase();if(!("stylesheet"===r||"modulepreload"===r||"preload"===r&&["script","style","font"].includes(i||"")))return;const c=l(s).attr("href"),a=c?V(c,t):void 0;a&&(l(s).attr("integrity",a),n&&l(s).attr("crossorigin",n))}));const u=new Set;l("link[rel='modulepreload'], link[rel='preload']").each(((e,t)=>{const s=l(t).attr("href");s&&u.add(s)}));const h=[];for(const e of s){const s=r?p.posix.join(r,e):`/${e}`;if(u.has(s))continue;const o=t[`/${e}`];if(!o)continue;let i=`<link rel="modulepreload" href="${s}" integrity="${o}"`;n&&(i+=` crossorigin="${n}"`),i+=">",h.push(i)}return h.length>0&&(l("head").append("\n "+h.join("\n ")),i&&console.log(`[SRI] Injected ${h.length} modulepreload links`)),l.html()}function J(e,t){try{const s=new Map(Object.entries(e||{})),r=t?.crossorigin,n=t?.skipResources||[],o=(e,t)=>{if(!e||!t)return!1;if(e===t)return!0;const s=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${s}$`).test(t)},i=e=>{for(const t of n){const s=e.getAttribute?.("id"),r=e.getAttribute?.("src"),n=e.getAttribute?.("href");if(s&&o(t,s)||r&&o(t,r)||n&&o(t,n))return!0}return!1},c=e=>{if(e)try{const t=new URL(e,globalThis.location?.href||"");return s.get(t.pathname)}catch{return}},l=e=>{if(!e||i(e))return;const t="undefined"!=typeof HTMLLinkElement&&e instanceof HTMLLinkElement,s="undefined"!=typeof HTMLScriptElement&&e instanceof HTMLScriptElement;if(!t&&!s)return;let n=null;if(t){const t=(e.rel||"").toLowerCase(),s=(e.getAttribute?.("as")||"").toLowerCase();if(!("stylesheet"===t||"modulepreload"===t||"preload"===t&&["script","style","font"].includes(s)))return;n=e.getAttribute?.("href")}else n=e.getAttribute?.("src");if(!n)return;const o=c(n);o&&e.setAttribute&&(e.hasAttribute("integrity")||e.setAttribute("integrity",o),r&&!e.hasAttribute("crossorigin")&&e.setAttribute("crossorigin",r))},a=Element?.prototype?.setAttribute;a&&(Element.prototype.setAttribute=function(e,t){const s=a.apply(this,arguments),r=String(e||"").toLowerCase();if("src"===r&&"script"===this.nodeName?.toLowerCase()||"href"===r&&"link"===this.nodeName?.toLowerCase()||"rel"===r||"as"===r)try{l(this)}catch{}return s});const u=Node?.prototype?.appendChild;u&&(Node.prototype.appendChild=function(e){const t=u.call(this,e);try{l(e)}catch{}return t});const p=Node?.prototype?.insertBefore;p&&(Node.prototype.insertBefore=function(e,t){const s=p.call(this,e,t);try{l(e)}catch{}return s});const h=Element?.prototype?.append;h&&(Element.prototype.append=function(...e){const t=h.apply(this,e);try{e.forEach((e=>l(e)))}catch{}return t});const d=Element?.prototype?.prepend;d&&(Element.prototype.prepend=function(...e){const t=d.apply(this,e);try{e.forEach((e=>l(e)))}catch{}return t})}catch{}}function q(e={}){const{algorithm:t="sha256",policy:s,dev:r={},features:n=D,build:o={},override:c=!1,debug:l=!1,transformPolicy:a}=e;let u,p,y,k,w=!1;const{outlierSupport:$=[],run:L=!1,override:j}=r,{sri:I=!1,outlierSupport:x=[],override:P}=o,O="object"==typeof I&&null!==I,R={enabled:"boolean"==typeof I?I:!!I,runtimePatchDynamicLinks:!O||(I.runtimePatchDynamicLinks??!0),preloadDynamicChunks:!O||(I.preloadDynamicChunks??!0),skipResources:O?I.skipResources??[]:[],crossorigin:O?I.crossorigin:void 0},M={"script-src":new Map,"script-src-attr":new Map,"script-src-elem":new Map,"style-src":new Map,"style-src-attr":new Map,"style-src-elem":new Map},T=(({userPolicy:e,override:t})=>{const s=e&&Object.keys(e).length>0;return!(t&&!s)})({userPolicy:s,override:c});if(!T)throw new Error("Override cannot be true when a csp policy is not provided");const V=L,z=()=>w&&V,B=new Map,q=new Map,U=(W=x,{postTransform:$.some((e=>f.includes(e))),strongLazyLoading:W.some((e=>m.includes(e)))});var W;const Y=C(s),G={algorithm:t,collection:M,policy:s||{},requirements:U,debug:l,isDevMode:w,shouldSkip:Y};return{name:"vite-plugin-csp-guard",enforce:"post",buildStart(){if(u=this,y=this.meta.viteVersion,!y)throw new Error("Please ensure you're using a minimum version of vite 8.0.0.")},apply:(e,{command:t})=>!("serve"!==t||"development"!==e.mode||!V)||("build"===t&&!e.build?.ssr||!("build"!==t||!n.mpa||!e.build?.ssr)),configResolved(e){k=e;const t="serve"===e.command&&"development"===e.mode;if(t&&!V&&console.warn("You are running in development mode but dev.run is set to false. This will not inject the default policy for development mode"),t&&(w=!0),"spa"!==e.appType&&!n.mpa)throw new Error("Vite CSP Plugin only works with SPA apps for now");if(e.build.ssr&&!n.mpa)throw new Error("Vite CSP Plugin does not work with SSR apps");G.isDevMode=w},load(e){if(!z())return null;const t=g(e),s=v(e),r=_(e),n=b(e);return(t||r||n||s)&&B.set(e,!1),null},transform:{order:U.postTransform?"post":"pre",handler:async(e,t)=>(n.mpa,z()?(await(async({code:e,id:t,cspContext:s,transformationStatus:r,transformMode:n,server:o})=>{const{algorithm:i,collection:c}=s;if(!o)return null;const l=g(t),a=v(t),u=_(t),p=b(t),h=()=>{const s="pre"===n?e:(e=>{const t=(e=>{const t=e.match(/const __vite__css\s*=\s*([\s\S]*?)(?=\s*__vite__updateStyle\(__vite__id,\s*__vite__css\))/);return t&&t[1]?t[1]:""})(e);return(e=>{let t=e.slice(1,-1);return t=t.replace(/\\\\:/g,"\\:"),t=t.replace(/\\n/g,"\n"),t=t.replace(/\\"/g,'"'),t})(t)})(e),o=S(s,i);E({hash:o,key:"style-src-elem",data:{algorithm:i,content:e},collection:c}),r.set(t,!0)},d=()=>{const s=S(e,i);E({hash:s,key:"script-src-elem",data:{algorithm:i,content:e},collection:c}),r.set(t,!0)};return r.has(t)?u||p?d():(l||a)&&h():u?d():(l||a)&&h(),Array.from(r.values()).every((e=>!0===e))&&(l||u)&&(await o.transformIndexHtml("/index.html","","/"),o.ws.send({type:"full-reload"})),null})({code:e,id:t,cspContext:G,transformationStatus:B,server:p,transformMode:U.postTransform?"post":"pre"}),null):null)},renderChunk:{order:"post",handler(e,s,r){if(z())return null;if(Y["script-src-elem"])return null;let n=e;e.includes("__VITE_PRELOAD__")&&l&&console.log(`Found __VITE_PRELOAD__ in chunk: ${s.fileName}`);const o=S(n,t);return q.set(s.fileName,o),l&&console.log(`Hashed chunk ${s.fileName}: sha256-${o.substring(0,20)}...`),null}},generateBundle:{order:"post",handler(e,s){if(!R.enabled||w)return;const r=s;try{l&&console.log("[SRI] Processing bundle for lazy chunk SRI...");let e=H(r,t,l);const n=[];if(R.runtimePatchDynamicLinks){const o=JSON.stringify(e),i=R.crossorigin?JSON.stringify(R.crossorigin):"undefined",c=JSON.stringify(R.skipResources),a=`\n(${J.toString()})(${o},{crossorigin:${i},skipResources:${c}});\n`;for(const[e,t]of Object.entries(s)){if("chunk"!==t.type)continue;const s=t;s.isEntry&&s.code&&(s.code=a+s.code,l&&console.log(`[SRI] Injected runtime into entry: ${e}`))}const u=H(r,t,l);for(const[t,r]of Object.entries(s)){if("chunk"!==r.type)continue;if(!r.isEntry)continue;const s=`/${t}`,o=e[s],i=u[s];o&&i&&o!==i&&n.push({old:o,new:i})}e=u}const o=function(e,t){const s=new Set,r=new Map,n=Object.values(e).filter((e=>"chunk"===e.type));for(const e of n)if(e.facadeModuleId&&r.set(e.facadeModuleId,e.fileName),e.name&&r.set(e.name,e.fileName),e.modules)for(const t of Object.keys(e.modules))r.set(t,e.fileName);for(const o of n)for(const i of o.dynamicImports||[]){const o=r.get(i)||("chunk"===e[i]?.type?e[i].fileName:null)||n.find((e=>e.name===i))?.fileName;o?s.add(o):t&&console.warn(`[SRI] Could not resolve dynamic import: ${i}`)}return t&&s.size>0&&console.log(`[SRI] Found ${s.size} dynamic chunks:`,[...s]),s}(r,l),i=k?.base??"/",c=i.endsWith("/")?i.slice(0,-1):i;for(const[t,r]of Object.entries(s)){if(!t.toLowerCase().endsWith(".html")||"asset"!==r.type)continue;const s=r;if(!s.source)continue;const i=F("string"==typeof s.source?s.source:Buffer.from(s.source).toString(),e,o,c,R.crossorigin,R.skipResources,l,n);s.source=i}}catch(e){console.error("[SRI] Error:",e)}}},transformIndexHtml:{order:"post",handler:async(e,t)=>{n.mpa;const r=z()?h:d,o=z()?j??c:P??c,l=i(r,s,o);return G.policy=l,A({html:e,context:t,pluginContext:u,isTransformationStatusEmpty:0===B.size,cspContext:G,sri:R.enabled,chunkHashes:q,transformPolicy:a})}},onLog(e,t){"vite-plugin-csp-guard"===t.plugin&&this.warn(t)},moduleParsed:e=>n.cssInJs?(({info:e})=>{if(e.id.includes("@emotion+sheet")){const t=N(e.ast,e.id);console.log("Has found styles: ",t)}})({info:e}):void 0,configureServer(e){p=e}}}export{q as default}; //# sourceMappingURL=index.esm.js.map