CSP.js 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. const crypto = require('crypto')
  2. const path = require('path')
  3. module.exports = function ({
  4. reportUri,
  5. reportPercentage,
  6. reportOnly = false,
  7. exclude = [],
  8. percentage,
  9. }) {
  10. return function (req, res, next) {
  11. const originalRender = res.render
  12. res.render = (...args) => {
  13. const view = relativeViewPath(args[0])
  14. // enable the CSP header for a percentage of requests
  15. const belowCutoff = Math.random() * 100 <= percentage
  16. if (belowCutoff && !exclude.includes(view)) {
  17. res.locals.cspEnabled = true
  18. const scriptNonce = crypto.randomBytes(16).toString('base64')
  19. res.locals.scriptNonce = scriptNonce
  20. const directives = [
  21. `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`,
  22. `object-src 'none'`,
  23. `base-uri 'none'`,
  24. ]
  25. // enable the report URI for a percentage of CSP-enabled requests
  26. const belowReportCutoff = Math.random() * 100 <= reportPercentage
  27. if (reportUri && belowReportCutoff) {
  28. directives.push(`report-uri ${reportUri}`)
  29. // NOTE: implement report-to once it's more widely supported
  30. }
  31. const policy = directives.join('; ')
  32. // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
  33. const header = reportOnly
  34. ? 'Content-Security-Policy-Report-Only'
  35. : 'Content-Security-Policy'
  36. res.set(header, policy)
  37. }
  38. originalRender.apply(res, args)
  39. }
  40. next()
  41. }
  42. }
  43. const webRoot = path.resolve(__dirname, '..', '..', '..')
  44. // build the view path relative to the web root
  45. function relativeViewPath(view) {
  46. return path.isAbsolute(view)
  47. ? path.relative(webRoot, view)
  48. : path.join('app', 'views', view)
  49. }