UNPKG

vite-plugin-csp-guard

Version:

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

3 lines (2 loc) 10.4 kB
import{createFilter as e,version as t}from"vite";import s from"crypto";import*as r from"cheerio";import*as n from"fs";import*as o from"path";const i={"default-src":["'self'"],"img-src":["'self'","data:"],"script-src-elem":["'self'"],"style-src-elem":["'self'"]},l={"default-src":["'self'"],"img-src":["'self'"],"script-src-elem":["'self'"],"style-src-elem":["'self'"]},c=["tailwind","sass","less","stylus","vue"],a=["vue-router"],p=({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)},u=e("**.css"),h=e(["**.scss","**.less","**.styl"]),m=e(["**/*.js?(*)","**/*.jsx?(*)"]),d=e(["**/*.ts","**/*.tsx"]);e("**.html");const f=(e,t)=>{const r=s.createHash(t);return r.update(e),r.digest("base64")},y=({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})}};function _({html:e,algorithm:t,collection:s,policy:n,bundleContext:o}){const i=r.load(e);return i("script").each((function(e,r){if(Object.keys(r.attribs).length&&r.attribs?.src?.length)try{const e=r.attribs.src;if((({currentPolicy:e,source:t,sourceType:s="script-src",context:r})=>{(e=>!(!e.includes("http://")&&!e.includes("https://")))(t)&&!p({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:n["script-src"]??[],sourceType:"script-src"}),o){const s=!!(l=e).startsWith("/")&&l.slice(1);if(s){const e=o[s];e&&i(r).attr("integrity",`${t}-${e.hash}`)}}}catch(e){console.error("Error hashing script src",e)}var l;if("text"===r.childNodes?.[0]?.type){const e=i.text([r.childNodes?.[0]]);if(e.length){const r=f(e,t);y({hash:r,key:"script-src-elem",data:{algorithm:t,content:e},collection:s})}}})),{HASH_COLLECTION:s,html:i.html()}}const v=({collection:e,policy:t})=>{const s={...t};for(const[t,r]of Object.entries(e)){const e=r,n=s[t]??[];n.includes("'unsafe-inline'")||e.size>0&&(s[t]=[...n,...Array.from(e.keys())])}const r=(e=>Object.keys(e).reduce(((t,s)=>{const r=e[s];return r?.length?`${t} ${s} ${r.map((e=>e.startsWith("sha")||e.startsWith("nonce")?`'${e}'`:e)).join(" ")};`:t}),"").trimStart())(s);return r},g=e=>e.replace(/__VITE_PRELOAD__/g,"[]"),w=(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},b=(e,t)=>{try{n.writeFileSync(o.resolve(process.cwd(),t),e),console.log(`Debug file written: ${t}`)}catch(e){console.error("Error writing debug file:",e)}},k=({html:e,context:{server:t,bundle:s,chunk:n,path:o,filename:i},pluginContext:l,isTransformationStatusEmpty:c,cspContext:a,sri:p})=>{const{algorithm:u,policy:h,collection:m,shouldSkip:d,isVite6:k,debug:S,requirements:x}=a;if(c&&t)return;const C={};if(s&&p)for(const e of Object.keys(s)){const t=s[e];if(t&&"chunk"===t.type&&!d["script-src-elem"]){let r=t.code;r.includes("__VITE_PRELOAD__")&&(S&&b(r,`temp-original-${e.replace(/\//g,"-")}.js`),r=x.strongLazyLoading?w(r,s):g(r),S&&b(r,`temp-transformed-${e.replace(/\//g,"-")}.js`));const n=f(r,u);m["script-src-elem"].has(n)||(y({hash:n,key:"script-src-elem",data:{algorithm:u,content:r},collection:m}),e.includes("index")&&(C[e]={type:"chunk",hash:n}))}}const{html:j,HASH_COLLECTION:P}=_({html:e,algorithm:u,collection:m,policy:h,bundleContext:s?C:void 0}),$=v({collection:P,policy:h}),E=(e=>[{tag:"meta",attrs:{"http-equiv":"Content-Security-Policy",content:e},injectTo:"head-prepend"}])($);if(k){const e=((e,t)=>{const s=r.load(e),n=`<meta http-equiv="Content-Security-Policy" content="${t}">`;return s("head").prepend(n),s.html()})(j,$);return{html:e,tags:[]}}return{html:j,tags:E}};class S{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 x extends S{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 l=this.should_skip,c=this.should_remove;if(this.should_skip=n,this.should_remove=o,this.replacement=i,l)return e;if(c)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];C(r)&&(this.visit(r,e,n,t)||t--)}}else C(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 C(e){return null!==e&&"object"==typeof e&&"type"in e&&"string"==typeof e.type}const j=(e,t)=>{if(!e)return!1;return function(e,{enter:t,leave:s}){new x(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},P={mpa:!1,cssInJs:!1};function $(e={}){const{algorithm:s="sha256",policy:r,dev:n={},features:o=P,build:p={},override:_=!1,debug:v=!1}=e;let g,w,b=!1;const{outlierSupport:S=[],run:x=!1}=n,{sri:C=!1,outlierSupport:$=[]}=p,E={"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},O=(({userPolicy:e,override:t})=>{const s=e&&Object.keys(e).length>0;return!(t&&!s)})({userPolicy:r,override:_});if(!O)throw new Error("Override cannot be true when a csp policy is not provided");const A=x,I=()=>b&&A,L=new Map,M=(T=$,{postTransform:S.some((e=>c.includes(e))),strongLazyLoading:T.some((e=>a.includes(e)))});var T;const D=(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};return e?(Object.keys(t).forEach((s=>{(e[s]?.includes("unsafe-inline")||e[s]?.includes("unsafe-eval"))&&(t[s]=!0)})),t):t})(r),H=t.split(".")[0];const N={algorithm:s,collection:E,policy:r||{},requirements:M,debug:v,isVite6:"6"===H,shouldSkip:D};return{name:"vite-plugin-csp-guard",enforce:"post",buildStart(){g=this},apply:(e,{command:t})=>!("serve"!==t||"development"!==e.mode||!A)||("build"===t&&!e.build?.ssr||!("build"!==t||!o.mpa||!e.build?.ssr)),configResolved(e){const t="serve"===e.command&&"development"===e.mode;if(t&&!A&&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&&(b=!0),"spa"!==e.appType&&!o.mpa)throw new Error("Vite CSP Plugin only works with SPA apps for now");if(e.build.ssr&&!o.mpa)throw new Error("Vite CSP Plugin does not work with SSR apps")},load(e){if(!I())return null;const t=u(e),s=h(e),r=m(e),n=d(e);return(t||r||n||s)&&L.set(e,!1),null},transform:{order:M.postTransform?"post":"pre",handler:async(e,t)=>(o.mpa,I()?(await(async({code:e,id:t,cspContext:s,transformationStatus:r,transformMode:n,server:o})=>{const{algorithm:i,collection:l}=s;if(!o)return null;const c=u(t),a=h(t),p=m(t),_=d(t),v=()=>{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=f(s,i);y({hash:o,key:"style-src-elem",data:{algorithm:i,content:e},collection:l}),r.set(t,!0)},g=()=>{const s=f(e,i);y({hash:s,key:"script-src-elem",data:{algorithm:i,content:e},collection:l}),r.set(t,!0)};return r.has(t)?p||_?g():(c||a)&&v():p?g():(c||a)&&v(),Array.from(r.values()).every((e=>!0===e))&&(c||p)&&(await o.transformIndexHtml("/index.html","","/"),o.ws.send({type:"full-reload"})),null})({code:e,id:t,cspContext:N,transformationStatus:L,server:w,transformMode:M.postTransform?"post":"pre"}),null):null)},transformIndexHtml:{order:"post",handler:async(e,t)=>{o.mpa;const s=((e,t,s)=>{const r=t&&Object.keys(t).length>0;if(s)return t;if(!r)return e;const n={...e};for(const s in t){const r=s;if(t.hasOwnProperty(s)){const s=e[r]||[],o=t[r]||[];Array.isArray(o)?n[r]=Array.from(new Set([...s,...o])):n[r]=o}}return n})(I()?i:l,r,_);return N.policy=s,k({html:e,context:t,pluginContext:g,isTransformationStatusEmpty:0===L.size,cspContext:N,sri:C})}},onLog(e,t){"vite-plugin-csp-guard"===t.plugin&&this.warn(t)},moduleParsed:e=>o.cssInJs?(({info:e})=>{if(e.id.includes("@emotion+sheet")){const t=j(e.ast,e.id);console.log("Has found styles: ",t)}})({info:e}):void 0,configureServer(e){w=e}}}export{$ as default}; //# sourceMappingURL=index.esm.js.map