[web] set a default, strict CSP on ALL endpoints (#6271)

* Remove use of CSP_PERCENTAGE

* Move header calculation earlier

* Set a default policy and add comments

* Apply the CSP header to all responses

* Enable CSP in dev environment

* [web] set a default, strict CSP on ALL endpoints

* [misc] enable CSP in dev-env

* Only build the default policy once

* Update docker-compose.yml

* [web] webpack: set default CSP header on webpack assets

This aligns the webpack dev-server with production in nocdn=true mode.

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
GitOrigin-RevId: 088a6082ad21c5b3f229887ba0ab3eca8d0528cd
This commit is contained in:
Jakob Ackermann 2022-03-17 09:23:07 +00:00 committed by Copybot
parent ecfe3df5ed
commit 224edddad4
4 changed files with 60 additions and 26 deletions

View file

@ -6,46 +6,37 @@ module.exports = function ({
reportPercentage,
reportOnly = false,
exclude = [],
percentage,
}) {
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)
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)) {
if (exclude.includes(view)) {
// remove the default policy
res.removeHeader(header)
} else {
// set the view policy
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'`,
]
// 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
const header = reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-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, '..', '..', '..')
// build the view path relative to the web root
@ -64,3 +92,5 @@ function relativeViewPath(view) {
? path.relative(webRoot, view)
: path.join('app', 'views', view)
}
module.exports.buildDefaultPolicy = buildDefaultPolicy

View file

@ -271,7 +271,7 @@ webRouter.use(
// add CSP header to HTML-rendering routes, if enabled
if (Settings.csp && Settings.csp.enabled) {
logger.info('adding CSP header to rendered routes', Settings.csp)
webRouter.use(csp(Settings.csp))
app.use(csp(Settings.csp))
}
logger.info('creating HTTP server'.yellow)

View file

@ -772,7 +772,6 @@ module.exports = {
moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],
csp: {
percentage: parseFloat(process.env.CSP_PERCENTAGE) || 0,
enabled: process.env.CSP_ENABLED === 'true',
reportOnly: process.env.CSP_REPORT_ONLY === 'true',
reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,

View file

@ -3,6 +3,7 @@ const merge = require('webpack-merge')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const base = require('./webpack.config')
const { buildDefaultPolicy } = require('./app/src/infrastructure/CSP')
module.exports = merge(base, {
mode: 'development',
@ -30,6 +31,10 @@ module.exports = merge(base, {
port: 3808,
public: 'www.dev-overleaf.com:443',
headers: {
'Content-Security-Policy': buildDefaultPolicy(),
},
// Customise output to the (node) console
stats: {
colors: true, // Enable some coloured highlighting