const crypto = require('crypto') const path = require('path') module.exports = function ({ reportUri, reportOnly = false, exclude = [], percentage }) { return function (req, res, next) { const originalRender = res.render res.render = (...args) => { const view = relativeViewPath(args[0]) // enable the CSP header for a percentage of requests const belowCutoff = Math.random() * 100 <= percentage if (belowCutoff && !exclude.includes(view)) { res.locals.cspEnabled = true const scriptNonce = crypto.randomBytes(16).toString('base64') res.locals.scriptNonce = scriptNonce const directives = [ `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, `object-src 'none'`, `base-uri 'none'` ] if (reportUri) { directives.push(`report-uri ${reportUri}`) // NOTE: implement report-to once it's more widely supported } const policy = directives.join('; ') // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy const header = reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy' res.set(header, policy) } originalRender.apply(res, args) } next() } } const webRoot = path.resolve(__dirname, '..', '..', '..') // build the view path relative to the web root function relativeViewPath(view) { return path.isAbsolute(view) ? path.relative(webRoot, view) : path.join('app', 'views', view) }