Skip to content

Commit

Permalink
Csp directive (#2005)
Browse files Browse the repository at this point in the history
* feat: add csp directives options

* fix: fix changes for adding CSP directives

* fix: remove unwanted space

* chore: add additional directives for CSP in routes.js

* fix: restrict user to set style-src & script-src in additional directives and example

* feat: expose getCSPHeader function for application to set CSP headers for their applicaiton

* fix: electrode/lint fix for return type

* fix: update name for variable and refactor code

* fix: fix lint issues and reformat code

* build: add changelog file
  • Loading branch information
RahulTinku authored Mar 4, 2024
1 parent 5b06cb0 commit c912c9f
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 32 deletions.
10 changes: 10 additions & 0 deletions common/changes/subapp-server/csp-directive_2024-03-01-04-51.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "subapp-server",
"comment": "Ablity to add CSP directives by application.",
"type": "minor"
}
],
"packageName": "subapp-server"
}
14 changes: 10 additions & 4 deletions packages/subapp-server/lib/fastify-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function makeRouteHandler({ path, routeRenderer, routeOptions }) {
await until(() => request.app.webpackDev.valid === true, 400);
console.log(`Webpack stats valid: ${request.app.webpackDev.valid}`);
}

const context = await routeRenderer({
useStream,
mode: "",
Expand All @@ -56,8 +56,15 @@ function makeRouteHandler({ path, routeRenderer, routeOptions }) {
const data = context.result;
const status = data.status;

const cspHeader = getCSPHeader({ styleNonce, scriptNonce });

let cspHeader;
/** If csp headers are provided by application in route options then use that otherwise generate CSP headers */
if (routeOptions.cspHeaderValues instanceof Function) {
const rawCSPHeader = routeOptions.cspHeaderValues({ 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);
}
Expand All @@ -77,7 +84,6 @@ function makeRouteHandler({ path, routeRenderer, routeOptions }) {
reply.code(status);
return reply.send(data);
}

} catch (err) {
reply.status(HttpStatusCodes.INTERNAL_SERVER_ERROR);
if (process.env.NODE_ENV !== "production") {
Expand Down
59 changes: 33 additions & 26 deletions packages/subapp-server/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ const updateFullTemplate = (baseDir, options) => {
};

function getDefaultRouteOptions() {
const { settings = {}, devServer = {}, fullDevServer = {}, httpDevServer = {} } = getDevProxy
? getDevProxy()
: {};
const {
settings = {},
devServer = {},
fullDevServer = {},
httpDevServer = {}
} = getDevProxy ? getDevProxy() : {};
const { webpackDev, useDevProxy } = settings;
// temporary location to write build artifacts in dev mode
const buildArtifacts = ".etmp";
Expand All @@ -42,9 +45,9 @@ function getDefaultRouteOptions() {
buildArtifacts,
prodBundleBase: "/js",
devBundleBase: "/js",
cspNonceValue: undefined,
cspNonceValue: undefined,
// if `true`, electrode will generate nonce and add CSP header
cspNonce: false,
cspNonce: false,
templateFile: Path.join(__dirname, "..", "resources", "index-page"),
cdn: {},
reporting: { enable: false }
Expand Down Expand Up @@ -143,46 +146,49 @@ function nonceGenerator(_) {
/**
* Sets CSP nonce to routeOptions and returns the nonce value
* cspNonce - boolean || object || string
*
* @param {*} param0
*
* @param {*} param0
* @returns nonce value
*/
function setCSPNonce({ routeOptions }) {
let nonce = nonceGenerator();
switch(typeof routeOptions.cspNonce) {

switch (typeof routeOptions.cspNonce) {
// if cspNonce is a string, electrode validates it for nonce value and uses the same to set CSP header
case "string": {
assert(routeOptions.cspNonce.match(/^[A-Za-z0-9+=\/]{22}$/), "Error: unable to set CSP header. Invalid nonce value passed!");
assert(
routeOptions.cspNonce.match(/^[A-Za-z0-9+=\/]{22}$/),
"Error: unable to set CSP header. Invalid nonce value passed!"
);

routeOptions.cspNonceValue = {
styleNonce: routeOptions.cspNonce,
scriptNonce: routeOptions.cspNonce
};
break;
};
}

// if cspNonce is true, electrode will generate nonce and sets CSP header for both
// if cspNonce is true, electrode will generate nonce and sets CSP header for both
// styles and script.
case "boolean": {
nonce = !!routeOptions.cspNonce === true ? nonce : ""
nonce = !!routeOptions.cspNonce === true ? nonce : "";
routeOptions.cspNonceValue = {
styleNonce: nonce,
scriptNonce: nonce
};
break;
};
// if cspHeader is an object, app should explicitly enable it for script and/or style.
}
// if cspHeader is an object, app should explicitly enable it for script and/or style.
// cspHeader: { style: true } - will enable nonce only for styles
case "object": {
routeOptions.cspNonceValue = {
styleNonce: routeOptions.cspNonce && !!routeOptions.cspNonce.style === true ? nonce : "",
scriptNonce: routeOptions.cspNonce && !!routeOptions.cspNonce.script === true ? nonce : ""
scriptNonce: routeOptions.cspNonce && !!routeOptions.cspNonce.script === true ? nonce : ""
};
break;
};
}
// TODO: add 'case' so that app can pass a nonce generator function.

default: {
routeOptions.cspNonceValue = {
styleNonce: "",
Expand All @@ -191,35 +197,36 @@ function setCSPNonce({ routeOptions }) {
break;
}
}

return routeOptions.cspNonceValue;
}

function getCSPHeader({ styleNonce = "", scriptNonce = "" }) {
const unsafeEval = process.env.NODE_ENV !== "production" ?
`'unsafe-eval'` : "";
const unsafeEval = process.env.NODE_ENV !== "production" ? `'unsafe-eval'` : "";

const styleSrc = styleNonce ? `style-src 'nonce-${styleNonce}' 'strict-dynamic';` : "";

const scriptSrc = scriptNonce ? `script-src 'nonce-${scriptNonce}' 'strict-dynamic' ${unsafeEval}; `: "";

const scriptSrc = scriptNonce
? `script-src 'nonce-${scriptNonce}' 'strict-dynamic' ${unsafeEval}; `
: "";

return `${scriptSrc}${styleSrc}`;
}

/**
* Wait for a condition and execute rest of the code.
* Wait for a condition and execute rest of the code.
* @param conditionFunction - A function that returns conditions to be waited for.
* @param maxWait - Max duration (in ms) to wait before promise resolves to avoid indefinite wait.
* @returns A promise that resolves after given condition in conditionFunction is satisfied or after the max wait time.
*/
function until(conditionFunction, maxWait) {
const poll = (resolve) => {
const poll = resolve => {
if (conditionFunction()) {
resolve();
} else {
setTimeout(_ => poll(resolve), maxWait);
}
}
};

return new Promise(poll);
}
Expand Down
21 changes: 19 additions & 2 deletions samples/poc-subappv1-csp/src/routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const path = require("path");
const { cspNonceValue } = require("./server/utils");

const subAppOptions = {
serverSideRendering: false,
};
Expand All @@ -10,6 +9,22 @@ const tokenHandlers = [path.join(__dirname, "./server/token-handler")];
const commonRouteOptions = {
tokenHandlers,
};
/**
*
* @param {string} styleNonce Value
* @param {string} scriptNonce Value
* @returns {string} CSP header value
*/
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;
};

/**
* To set CSP header
Expand All @@ -20,6 +35,7 @@ const commonRouteOptions = {
*
* Option 3 - Selectively set boolean flag for `cspNonce`. { style: true } will add nonce only
* for styles
*
*/

export default {
Expand All @@ -30,7 +46,8 @@ export default {
// Enable one of these to use CSP header
cspNonce: true,
// cspNonce: { style: true }, // { script: true }
// cspNonce: cspNonceValue,
// cspNonce: cspNonceValue,
cspHeaderValues: setCSPHeaderValues,
criticalCSS: path.join(__dirname, "./server/critical.css"),
...commonRouteOptions
}
Expand Down

0 comments on commit c912c9f

Please sign in to comment.