CSP.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. const crypto = require('crypto')
  2. const path = require('path')
  3. module.exports = function ({
  4. reportUri,
  5. reportPercentage,
  6. reportOnly = false,
  7. exclude = [],
  8. viewDirectives = {},
  9. }) {
  10. const header = reportOnly
  11. ? 'Content-Security-Policy-Report-Only'
  12. : 'Content-Security-Policy'
  13. const defaultPolicy = buildDefaultPolicy(reportUri)
  14. return function (req, res, next) {
  15. // set the default policy
  16. res.set(header, defaultPolicy)
  17. if (reportUri) {
  18. res.set('Reporting-Endpoints', `csp-endpoint="${reportUri}"`)
  19. }
  20. const originalRender = res.render
  21. res.render = (...args) => {
  22. const view = relativeViewPath(args[0])
  23. if (exclude.includes(view)) {
  24. // remove the default policy
  25. res.removeHeader(header)
  26. res.removeHeader('Reporting-Endpoints')
  27. } else {
  28. // set the view policy
  29. res.locals.cspEnabled = true
  30. const scriptNonce = crypto.randomBytes(16).toString('base64')
  31. res.locals.scriptNonce = scriptNonce
  32. const policy = buildViewPolicy(
  33. scriptNonce,
  34. reportPercentage,
  35. reportUri,
  36. viewDirectives[view]
  37. )
  38. // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
  39. res.set(header, policy)
  40. }
  41. originalRender.apply(res, args)
  42. }
  43. next()
  44. }
  45. }
  46. const buildDefaultPolicy = (reportUri, styleSrc) => {
  47. const directives = [
  48. `base-uri 'none'`, // forbid setting a "base" element
  49. `default-src 'none'`, // forbid loading anything from a "src" attribute
  50. `form-action 'none'`, // forbid setting a form action
  51. `frame-ancestors 'none'`, // forbid loading embedded content
  52. `img-src 'self'`, // allow loading images from the same domain (e.g. the favicon).
  53. ]
  54. if (reportUri) {
  55. directives.push(`report-uri ${reportUri}`)
  56. directives.push(`report-to csp-endpoint`)
  57. }
  58. if (styleSrc) {
  59. directives.push(`style-src ${styleSrc}`)
  60. }
  61. return directives.join('; ')
  62. }
  63. const buildViewPolicy = (
  64. scriptNonce,
  65. reportPercentage,
  66. reportUri,
  67. viewDirectives
  68. ) => {
  69. const directives = [
  70. `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, // only allow scripts from certain sources
  71. `object-src 'none'`, // forbid loading an "object" element
  72. `base-uri 'none'`, // forbid setting a "base" element
  73. ...(viewDirectives ?? []),
  74. ]
  75. if (reportUri) {
  76. // enable the report URI for a percentage of CSP-enabled requests
  77. const belowReportCutoff = Math.random() * 100 <= reportPercentage
  78. if (belowReportCutoff) {
  79. directives.push(`report-uri ${reportUri}`)
  80. directives.push(`report-to csp-endpoint`)
  81. }
  82. }
  83. return directives.join('; ')
  84. }
  85. const webRoot = path.resolve(__dirname, '..', '..', '..')
  86. // build the view path relative to the web root
  87. function relativeViewPath(view) {
  88. return path.isAbsolute(view)
  89. ? path.relative(webRoot, view)
  90. : path.join('app', 'views', view)
  91. }
  92. function removeCSPHeaders(res) {
  93. res.removeHeader('Content-Security-Policy')
  94. res.removeHeader('Content-Security-Policy-Report-Only')
  95. }
  96. /**
  97. * WARNING: allowing inline styles can open a security hole;
  98. * this is intended only for use in specific circumstances, such as Safari's built-in PDF viewer.
  99. */
  100. function allowUnsafeInlineStyles(res) {
  101. res.set(
  102. 'Content-Security-Policy',
  103. buildDefaultPolicy(undefined, "'unsafe-inline'")
  104. )
  105. }
  106. module.exports.buildDefaultPolicy = buildDefaultPolicy
  107. module.exports.removeCSPHeaders = removeCSPHeaders
  108. module.exports.allowUnsafeInlineStyles = allowUnsafeInlineStyles