UNPKG

vite-plugin-csp-guard

Version:

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

3 lines (2 loc) 10.7 kB
"use strict";var e=require("vite"),t=require("crypto"),s=require("cheerio"),r=require("fs"),n=require("path");function o(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(s){if("default"!==s){var r=Object.getOwnPropertyDescriptor(e,s);Object.defineProperty(t,s,r.get?r:{enumerable:!0,get:function(){return e[s]}})}})),t.default=e,Object.freeze(t)}var i=o(s),c=o(r),l=o(n);const a={"default-src":["'self'"],"img-src":["'self'","data:"],"script-src-elem":["'self'"],"style-src-elem":["'self'"]},u={"default-src":["'self'"],"img-src":["'self'"],"script-src-elem":["'self'"],"style-src-elem":["'self'"]},p=["tailwind","sass","less","stylus","vue"],h=["vue-router"],d=({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)},m=e.createFilter("**.css"),f=e.createFilter(["**.scss","**.less","**.styl"]),y=e.createFilter(["**/*.js?(*)","**/*.jsx?(*)"]),v=e.createFilter(["**/*.ts","**/*.tsx"]);e.createFilter("**.html");const _=(e,s)=>{const r=t.createHash(s);return r.update(e),r.digest("base64")},g=({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 w({html:e,algorithm:t,collection:s,policy:r,bundleContext:n}){const o=i.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)&&!d({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=_(e,t);g({hash:r,key:"script-src-elem",data:{algorithm:t,content:e},collection:s})}}})),{HASH_COLLECTION:s,html:o.html()}}const b=({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},k=e=>e.replace(/__VITE_PRELOAD__/g,"[]"),S=(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},x=(e,t)=>{try{c.writeFileSync(l.resolve(process.cwd(),t),e),console.log(`Debug file written: ${t}`)}catch(e){console.error("Error writing debug file:",e)}},j=({html:e,context:{server:t,bundle:s,chunk:r,path:n,filename:o},pluginContext:c,isTransformationStatusEmpty:l,cspContext:a,sri:u})=>{const{algorithm:p,policy:h,collection:d,shouldSkip:m,isVite6:f,debug:y,requirements:v}=a;if(l&&t)return;const j={};if(s&&u)for(const e of Object.keys(s)){const t=s[e];if(t&&"chunk"===t.type&&!m["script-src-elem"]){let r=t.code;r.includes("__VITE_PRELOAD__")&&(y&&x(r,`temp-original-${e.replace(/\//g,"-")}.js`),r=v.strongLazyLoading?S(r,s):k(r),y&&x(r,`temp-transformed-${e.replace(/\//g,"-")}.js`));const n=_(r,p);d["script-src-elem"].has(n)||(g({hash:n,key:"script-src-elem",data:{algorithm:p,content:r},collection:d}),e.includes("index")&&(j[e]={type:"chunk",hash:n}))}}const{html:C,HASH_COLLECTION:O}=w({html:e,algorithm:p,collection:d,policy:h,bundleContext:s?j:void 0}),P=b({collection:O,policy:h}),E=(e=>[{tag:"meta",attrs:{"http-equiv":"Content-Security-Policy",content:e},injectTo:"head-prepend"}])(P);if(f){const e=((e,t)=>{const s=i.load(e),r=`<meta http-equiv="Content-Security-Policy" content="${t}">`;return s("head").prepend(r),s.html()})(C,P);return{html:e,tags:[]}}return{html:C,tags:E}};class C{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 O extends C{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];P(r)&&(this.visit(r,e,n,t)||t--)}}else P(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 P(e){return null!==e&&"object"==typeof e&&"type"in e&&"string"==typeof e.type}const E=(e,t)=>{if(!e)return!1;return function(e,{enter:t,leave:s}){new O(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},$={mpa:!1,cssInJs:!1};module.exports=function(t={}){const{algorithm:s="sha256",policy:r,dev:n={},features:o=$,build:i={},override:c=!1,debug:l=!1}=t;let d,w,b=!1;const{outlierSupport:k=[],run:S=!1}=n,{sri:x=!1,outlierSupport:C=[]}=i,O={"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};if(!(({userPolicy:e,override:t})=>{const s=e&&Object.keys(e).length>0;return!(t&&!s)})({userPolicy:r,override:c}))throw new Error("Override cannot be true when a csp policy is not provided");const P=S,A=()=>b&&P,I=new Map,L=(M=C,{postTransform:k.some((e=>p.includes(e))),strongLazyLoading:M.some((e=>h.includes(e)))});var M;const T=(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),D=e.version.split(".")[0],q={algorithm:s,collection:O,policy:r||{},requirements:L,debug:l,isVite6:"6"===D,shouldSkip:T};return{name:"vite-plugin-csp-guard",enforce:"post",buildStart(){d=this},apply:(e,{command:t})=>!("serve"!==t||"development"!==e.mode||!P)||("build"===t&&!e.build?.ssr||!("build"!==t||!o.mpa||!e.build?.ssr)),configResolved(e){const t="serve"===e.command&&"development"===e.mode;if(t&&!P&&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(!A())return null;const t=m(e),s=f(e),r=y(e),n=v(e);return(t||r||n||s)&&I.set(e,!1),null},transform:{order:L.postTransform?"post":"pre",handler:async(e,t)=>(o.mpa,A()?(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=m(t),a=f(t),u=y(t),p=v(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,i);g({hash:o,key:"style-src-elem",data:{algorithm:i,content:e},collection:c}),r.set(t,!0)},d=()=>{const s=_(e,i);g({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:q,transformationStatus:I,server:w,transformMode:L.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})(A()?a:u,r,c);return q.policy=s,j({html:e,context:t,pluginContext:d,isTransformationStatusEmpty:0===I.size,cspContext:q,sri:x})}},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=E(e.ast,e.id);console.log("Has found styles: ",t)}})({info:e}):void 0,configureServer(e){w=e}}}; //# sourceMappingURL=index.cjs.js.map