Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Csp directive #2005

Merged
merged 10 commits into from
Mar 4, 2024
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,
RahulTinku marked this conversation as resolved.
Show resolved Hide resolved
// cspNonce: { style: true }, // { script: true }
// cspNonce: cspNonceValue,
// cspNonce: cspNonceValue,
cspHeaderValues: setCSPHeaderValues,
criticalCSS: path.join(__dirname, "./server/critical.css"),
...commonRouteOptions
}
Expand Down
Loading