| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- const crypto = require('crypto')
- const path = require('path')
- module.exports = function ({
- reportUri,
- reportPercentage,
- reportOnly = false,
- exclude = [],
- viewDirectives = {},
- }) {
- const header = reportOnly
- ? 'Content-Security-Policy-Report-Only'
- : 'Content-Security-Policy'
- const defaultPolicy = buildDefaultPolicy(reportUri)
- return function (req, res, next) {
- // set the default policy
- res.set(header, defaultPolicy)
- if (reportUri) {
- res.set('Reporting-Endpoints', `csp-endpoint="${reportUri}"`)
- }
- const originalRender = res.render
- res.render = (...args) => {
- const view = relativeViewPath(args[0])
- if (exclude.includes(view)) {
- // remove the default policy
- res.removeHeader(header)
- res.removeHeader('Reporting-Endpoints')
- } else {
- // set the view policy
- res.locals.cspEnabled = true
- const scriptNonce = crypto.randomBytes(16).toString('base64')
- res.locals.scriptNonce = scriptNonce
- const policy = buildViewPolicy(
- scriptNonce,
- reportPercentage,
- reportUri,
- viewDirectives[view]
- )
- // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
- res.set(header, policy)
- }
- originalRender.apply(res, args)
- }
- next()
- }
- }
- const buildDefaultPolicy = (reportUri, styleSrc) => {
- 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}`)
- directives.push(`report-to csp-endpoint`)
- }
- if (styleSrc) {
- directives.push(`style-src ${styleSrc}`)
- }
- return directives.join('; ')
- }
- const buildViewPolicy = (
- scriptNonce,
- reportPercentage,
- reportUri,
- viewDirectives
- ) => {
- 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
- ...(viewDirectives ?? []),
- ]
- 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}`)
- directives.push(`report-to csp-endpoint`)
- }
- }
- return directives.join('; ')
- }
- 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)
- }
- function removeCSPHeaders(res) {
- res.removeHeader('Content-Security-Policy')
- res.removeHeader('Content-Security-Policy-Report-Only')
- }
- /**
- * WARNING: allowing inline styles can open a security hole;
- * this is intended only for use in specific circumstances, such as Safari's built-in PDF viewer.
- */
- function allowUnsafeInlineStyles(res) {
- res.set(
- 'Content-Security-Policy',
- buildDefaultPolicy(undefined, "'unsafe-inline'")
- )
- }
- module.exports.buildDefaultPolicy = buildDefaultPolicy
- module.exports.removeCSPHeaders = removeCSPHeaders
- module.exports.allowUnsafeInlineStyles = allowUnsafeInlineStyles
|