From e644bbe4418a1ade930d3b945e429fad7541fe99 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Wed, 28 Feb 2024 12:59:46 +0530 Subject: [PATCH] feat: expose getCSPHeader function for application to set CSP headers for their applicaiton --- packages/subapp-server/lib/fastify-plugin.js | 15 ++++++---- packages/subapp-server/lib/utils.js | 29 +++----------------- samples/poc-subappv1-csp/src/routes.js | 28 +++++++++++-------- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/packages/subapp-server/lib/fastify-plugin.js b/packages/subapp-server/lib/fastify-plugin.js index 17f50bb49..80f313513 100644 --- a/packages/subapp-server/lib/fastify-plugin.js +++ b/packages/subapp-server/lib/fastify-plugin.js @@ -25,8 +25,7 @@ const { updateFullTemplate, setCSPNonce, getCSPHeader, - until, - setCSPDirectives + until } = require("./utils"); const routesFromFile = require("./routes-from-file"); @@ -41,7 +40,6 @@ function makeRouteHandler({ path, routeRenderer, routeOptions }) { return async (request, reply) => { try { const { styleNonce = "", scriptNonce = ""} = setCSPNonce({ routeOptions }); - const directiveNonce = setCSPDirectives({routeOptions}) // wait for webpack stats to be valid if webpackDev if (webpackDev) { @@ -58,8 +56,15 @@ function makeRouteHandler({ path, routeRenderer, routeOptions }) { const data = context.result; const status = data.status; - const cspHeader = getCSPHeader({ styleNonce, scriptNonce, directiveNonce }); - + let cspHeader; + /** If csp headers are provided by application in route options then use that otherwise generate CSP headers */ + if(routeOptions.getCSPHeader && typeof routeOptions.getCSPHeader === "function"){ + const rawCSPHeader = routeOptions.getCSPHeader({ styleNonce, scriptNonce }); + // Replace newline characters and spaces + cspHeader = rawCSPHeader.replace(/\s{2,}/g, " ").trim(); + }else{ + cspHeader = getCSPHeader({ styleNonce, scriptNonce }); + } if (cspHeader) { reply.header("Content-Security-Policy", cspHeader); } diff --git a/packages/subapp-server/lib/utils.js b/packages/subapp-server/lib/utils.js index c36a7e3aa..cc9341dda 100644 --- a/packages/subapp-server/lib/utils.js +++ b/packages/subapp-server/lib/utils.js @@ -47,9 +47,7 @@ function getDefaultRouteOptions() { cspNonce: false, templateFile: Path.join(__dirname, "..", "resources", "index-page"), cdn: {}, - reporting: { enable: false }, - cspDirectives: undefined, - cspDirectivesValue : undefined + reporting: { enable: false } }; } @@ -197,23 +195,7 @@ function setCSPNonce({ routeOptions }) { return routeOptions.cspNonceValue; } -function setCSPDirectives({routeOptions}){ - /** - * Check if cspDirectives is present in routerOptions and cspDirectives is an Object - */ - if(routeOptions.cspDirectives && typeof routeOptions.cspDirectives === "object"){ - const data = Object.entries(routeOptions.cspDirectives).filter(([key,value]) => { - /** Check if script-src or style-src is explicitly set as additional directives */ - if(key !== 'script-src' && key !== 'style-src'){ - return ` ${key} ${value}` - } - }); - routeOptions.cspDirectivesValue = data.join("; "); - } - return routeOptions.cspDirectivesValue; -} - -function getCSPHeader({ styleNonce = "", scriptNonce = "", directiveNonce = "" }) { +function getCSPHeader({ styleNonce = "", scriptNonce = "" }) { const unsafeEval = process.env.NODE_ENV !== "production" ? `'unsafe-eval'` : ""; @@ -221,9 +203,7 @@ function getCSPHeader({ styleNonce = "", scriptNonce = "", directiveNonce = "" } const scriptSrc = scriptNonce ? `script-src 'nonce-${scriptNonce}' 'strict-dynamic' ${unsafeEval}; `: ""; - const directiveSrc = directiveNonce ? `${directiveNonce};`: ""; - - return `${scriptSrc}${styleSrc}${directiveSrc}`; + return `${scriptSrc}${styleSrc}`; } /** @@ -254,6 +234,5 @@ module.exports = { invokeTemplateProcessor, setCSPNonce, getCSPHeader, - until, - setCSPDirectives + until }; diff --git a/samples/poc-subappv1-csp/src/routes.js b/samples/poc-subappv1-csp/src/routes.js index 8dcc30585..a5bfc4fff 100644 --- a/samples/poc-subappv1-csp/src/routes.js +++ b/samples/poc-subappv1-csp/src/routes.js @@ -1,6 +1,5 @@ const path = require("path"); const { cspNonceValue } = require("./server/utils"); - const subAppOptions = { serverSideRendering: false, }; @@ -11,15 +10,19 @@ const commonRouteOptions = { tokenHandlers, }; /** - * to set CSP directives pass the cspDirectives as an object - * key should be the name of the directive - * value should be the value of the directive. + * + * @param {*} param0 + * @returns CSP header value as string */ -const additionalDirective = { - "frame-src": "'self' allowed-site.example.com", - "prefetch-src": "'none'", - "manifest-src": "'none'", - "script-src": "'self" +const setCSPHeaderValues = ({styleNonce, scriptNonce}) => { + const cspHeader = ` + script-src 'self' 'nonce-${scriptNonce}' 'strict-dynamic' 'unsafe-eval'; + style-src 'self' 'nonce-${styleNonce}' 'strict-dynamic' 'unsafe-eval'; + font-src 'self'; + object-src 'none'; + form-action 'self'; + `; + return cspHeader; }; /** @@ -31,6 +34,9 @@ const additionalDirective = { * * Option 3 - Selectively set boolean flag for `cspNonce`. { style: true } will add nonce only * for styles + * + * Option 4 - write a function which would return list of CSP directives and values as string + * and pass that "function" to getCSPHeader */ export default { @@ -41,8 +47,8 @@ export default { // Enable one of these to use CSP header cspNonce: true, // cspNonce: { style: true }, // { script: true } - // cspNonce: cspNonceValue, - cspDirectives: additionalDirective, + // cspNonce: cspNonceValue, + getCSPHeader: setCSPHeaderValues, criticalCSS: path.join(__dirname, "./server/critical.css"), ...commonRouteOptions }