|
@@ -6,46 +6,37 @@ module.exports = function ({
|
|
|
reportPercentage,
|
|
reportPercentage,
|
|
|
reportOnly = false,
|
|
reportOnly = false,
|
|
|
exclude = [],
|
|
exclude = [],
|
|
|
- percentage,
|
|
|
|
|
}) {
|
|
}) {
|
|
|
|
|
+ const header = reportOnly
|
|
|
|
|
+ ? 'Content-Security-Policy-Report-Only'
|
|
|
|
|
+ : 'Content-Security-Policy'
|
|
|
|
|
+
|
|
|
|
|
+ const defaultPolicy = buildDefaultPolicy(reportUri)
|
|
|
|
|
+
|
|
|
return function (req, res, next) {
|
|
return function (req, res, next) {
|
|
|
|
|
+ // set the default policy
|
|
|
|
|
+ res.set(header, defaultPolicy)
|
|
|
|
|
+
|
|
|
const originalRender = res.render
|
|
const originalRender = res.render
|
|
|
|
|
|
|
|
res.render = (...args) => {
|
|
res.render = (...args) => {
|
|
|
const view = relativeViewPath(args[0])
|
|
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)) {
|
|
|
|
|
|
|
+ if (exclude.includes(view)) {
|
|
|
|
|
+ // remove the default policy
|
|
|
|
|
+ res.removeHeader(header)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // set the view policy
|
|
|
res.locals.cspEnabled = true
|
|
res.locals.cspEnabled = true
|
|
|
|
|
|
|
|
const scriptNonce = crypto.randomBytes(16).toString('base64')
|
|
const scriptNonce = crypto.randomBytes(16).toString('base64')
|
|
|
|
|
|
|
|
res.locals.scriptNonce = scriptNonce
|
|
res.locals.scriptNonce = scriptNonce
|
|
|
|
|
|
|
|
- const directives = [
|
|
|
|
|
- `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`,
|
|
|
|
|
- `object-src 'none'`,
|
|
|
|
|
- `base-uri 'none'`,
|
|
|
|
|
- ]
|
|
|
|
|
-
|
|
|
|
|
- // enable the report URI for a percentage of CSP-enabled requests
|
|
|
|
|
- const belowReportCutoff = Math.random() * 100 <= reportPercentage
|
|
|
|
|
-
|
|
|
|
|
- if (reportUri && belowReportCutoff) {
|
|
|
|
|
- directives.push(`report-uri ${reportUri}`)
|
|
|
|
|
- // NOTE: implement report-to once it's more widely supported
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const policy = directives.join('; ')
|
|
|
|
|
|
|
+ const policy = buildViewPolicy(scriptNonce, reportPercentage, reportUri)
|
|
|
|
|
|
|
|
// Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
|
|
// 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)
|
|
res.set(header, policy)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -56,6 +47,43 @@ module.exports = function ({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const buildDefaultPolicy = reportUri => {
|
|
|
|
|
+ const directives = [
|
|
|
|
|
+ `base-uri 'none'`, // forbid setting a "base" element
|
|
|
|
|
+ `default-src 'none'`, // forbid loading anything from a "src" attribute
|
|
|
|
|
+ `form-action 'none'`, // forbid setting a form action
|
|
|
|
|
+ `frame-ancestors 'none'`, // forbid loading embedded content
|
|
|
|
|
+ `img-src 'self'`, // allow loading images from the same domain (e.g. the favicon).
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ if (reportUri) {
|
|
|
|
|
+ directives.push(`report-uri ${reportUri}`)
|
|
|
|
|
+ // NOTE: implement report-to once it's more widely supported
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return directives.join('; ')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const buildViewPolicy = (scriptNonce, reportPercentage, reportUri) => {
|
|
|
|
|
+ const directives = [
|
|
|
|
|
+ `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, // only allow scripts from certain sources
|
|
|
|
|
+ `object-src 'none'`, // forbid loading an "object" element
|
|
|
|
|
+ `base-uri 'none'`, // forbid setting a "base" element
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ if (reportUri) {
|
|
|
|
|
+ // enable the report URI for a percentage of CSP-enabled requests
|
|
|
|
|
+ const belowReportCutoff = Math.random() * 100 <= reportPercentage
|
|
|
|
|
+
|
|
|
|
|
+ if (belowReportCutoff) {
|
|
|
|
|
+ directives.push(`report-uri ${reportUri}`)
|
|
|
|
|
+ // NOTE: implement report-to once it's more widely supported
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return directives.join('; ')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const webRoot = path.resolve(__dirname, '..', '..', '..')
|
|
const webRoot = path.resolve(__dirname, '..', '..', '..')
|
|
|
|
|
|
|
|
// build the view path relative to the web root
|
|
// build the view path relative to the web root
|
|
@@ -64,3 +92,5 @@ function relativeViewPath(view) {
|
|
|
? path.relative(webRoot, view)
|
|
? path.relative(webRoot, view)
|
|
|
: path.join('app', 'views', view)
|
|
: path.join('app', 'views', view)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+module.exports.buildDefaultPolicy = buildDefaultPolicy
|