From ba183ce6543f102ae635502a0da0ac7c923cc97a Mon Sep 17 00:00:00 2001 From: Literallie Date: Wed, 18 Oct 2017 17:10:23 +0200 Subject: Add basic CSP support --- app.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'app.js') diff --git a/app.js b/app.js index 62e6627d..54ec6cf7 100644 --- a/app.js +++ b/app.js @@ -108,6 +108,31 @@ if (config.hsts.enable) { logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security') } +// use Content-Security-Policy to limit XSS, dangerous plugins, etc. +// https://helmetjs.github.io/docs/csp/ +if (config.csp.enable) { + var cdnDirectives = { + scriptSrc: ["https://cdnjs.cloudflare.com"], + styleSrc: ["https://cdnjs.cloudflare.com", "https://fonts.googleapis.com"], + fontSrc: ["https://cdnjs.cloudflare.com", "https://fonts.gstatic.com"] + } + var directives = {} + for (var propertyName in config.csp.directives) { + if(config.csp.directives.hasOwnProperty(propertyName)) { + var directive = config.csp.directives[propertyName] + if (config.usecdn && !!cdnDirectives[propertyName]) { + directive = directive.concat(cdnDirectives[propertyName]) + } + directives[propertyName] = directive; + } + } + app.use(helmet.contentSecurityPolicy({ + directives: directives + })) +} else { + logger.info('Content-Security-Policy is disabled. This may be a security risk.'); +} + i18n.configure({ locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da'], cookie: 'locale', -- cgit v1.2.3 From 5d2d3ec875310de07fe79ae605dfbc0f1df585c5 Mon Sep 17 00:00:00 2001 From: Literallie Date: Wed, 18 Oct 2017 17:45:57 +0200 Subject: CSP: Upgrade insecure requests if possible Config option; default is to only upgrade if usessl --- app.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'app.js') diff --git a/app.js b/app.js index 54ec6cf7..8af029e7 100644 --- a/app.js +++ b/app.js @@ -126,6 +126,11 @@ if (config.csp.enable) { directives[propertyName] = directive; } } + if(config.csp.upgradeInsecureRequests === 'auto') { + directives.upgradeInsecureRequests = config.usessl === 'true' + } else { + directives.upgradeInsecureRequests = config.csp.upgradeInsecureRequests === 'true' + } app.use(helmet.contentSecurityPolicy({ directives: directives })) -- cgit v1.2.3 From 080436aebb4c4681f85cc8bf5d8563832ff8dbdd Mon Sep 17 00:00:00 2001 From: Literallie Date: Wed, 18 Oct 2017 17:48:53 +0200 Subject: CSP: Add nonce to slide view inline JS --- app.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'app.js') diff --git a/app.js b/app.js index 8af029e7..b78f94e1 100644 --- a/app.js +++ b/app.js @@ -12,6 +12,7 @@ var session = require('express-session') var SequelizeStore = require('connect-session-sequelize')(session.Store) var fs = require('fs') var path = require('path') +var uuid = require('uuid') var morgan = require('morgan') var passportSocketIo = require('passport.socketio') @@ -108,6 +109,11 @@ if (config.hsts.enable) { logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security') } +app.use((req, res, next) => { + res.locals.nonce = uuid.v4() + next() +}) + // use Content-Security-Policy to limit XSS, dangerous plugins, etc. // https://helmetjs.github.io/docs/csp/ if (config.csp.enable) { @@ -126,6 +132,7 @@ if (config.csp.enable) { directives[propertyName] = directive; } } + directives.scriptSrc.push(function (req, res) { return "'nonce-" + res.locals.nonce + "'" }) if(config.csp.upgradeInsecureRequests === 'auto') { directives.upgradeInsecureRequests = config.usessl === 'true' } else { -- cgit v1.2.3 From 4238b9b3efc3a9c001b5e1cf6c1883ffd0f92800 Mon Sep 17 00:00:00 2001 From: Literallie Date: Wed, 18 Oct 2017 19:37:55 +0200 Subject: Fix MathJax CSP issues --- app.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'app.js') diff --git a/app.js b/app.js index b78f94e1..88735854 100644 --- a/app.js +++ b/app.js @@ -118,22 +118,22 @@ app.use((req, res, next) => { // https://helmetjs.github.io/docs/csp/ if (config.csp.enable) { var cdnDirectives = { - scriptSrc: ["https://cdnjs.cloudflare.com"], - styleSrc: ["https://cdnjs.cloudflare.com", "https://fonts.googleapis.com"], - fontSrc: ["https://cdnjs.cloudflare.com", "https://fonts.gstatic.com"] + scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], + styleSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com'], + fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com'] } var directives = {} for (var propertyName in config.csp.directives) { - if(config.csp.directives.hasOwnProperty(propertyName)) { + if (config.csp.directives.hasOwnProperty(propertyName)) { var directive = config.csp.directives[propertyName] if (config.usecdn && !!cdnDirectives[propertyName]) { directive = directive.concat(cdnDirectives[propertyName]) } - directives[propertyName] = directive; + directives[propertyName] = directive } } directives.scriptSrc.push(function (req, res) { return "'nonce-" + res.locals.nonce + "'" }) - if(config.csp.upgradeInsecureRequests === 'auto') { + if (config.csp.upgradeInsecureRequests === 'auto') { directives.upgradeInsecureRequests = config.usessl === 'true' } else { directives.upgradeInsecureRequests = config.csp.upgradeInsecureRequests === 'true' @@ -142,7 +142,7 @@ if (config.csp.enable) { directives: directives })) } else { - logger.info('Content-Security-Policy is disabled. This may be a security risk.'); + logger.info('Content-Security-Policy is disabled. This may be a security risk.') } i18n.configure({ -- cgit v1.2.3 From 996cb379912d5ee7b6e26f3c688ce447b4762bc4 Mon Sep 17 00:00:00 2001 From: Literallie Date: Wed, 18 Oct 2017 22:45:17 +0200 Subject: CSP: Workaround for ws:// protocol The spec allows wss:// for 'self', but not ws:// :( --- app.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'app.js') diff --git a/app.js b/app.js index 88735854..15c9e61e 100644 --- a/app.js +++ b/app.js @@ -116,6 +116,15 @@ app.use((req, res, next) => { // use Content-Security-Policy to limit XSS, dangerous plugins, etc. // https://helmetjs.github.io/docs/csp/ +function getCspNonce (req, res) { + return "'nonce-" + res.locals.nonce + "'" +} + +function getCspWebSocketUrl (req, res) { + // wss: is included in 'self', but 'ws:' is not + return (req.protocol === 'http' ? 'ws:' : 'wss:') + config.serverurl.replace(/https?:/, "") +} + if (config.csp.enable) { var cdnDirectives = { scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], @@ -125,14 +134,15 @@ if (config.csp.enable) { var directives = {} for (var propertyName in config.csp.directives) { if (config.csp.directives.hasOwnProperty(propertyName)) { - var directive = config.csp.directives[propertyName] + var directive = [].concat(config.csp.directives[propertyName]) if (config.usecdn && !!cdnDirectives[propertyName]) { directive = directive.concat(cdnDirectives[propertyName]) } directives[propertyName] = directive } } - directives.scriptSrc.push(function (req, res) { return "'nonce-" + res.locals.nonce + "'" }) + directives.scriptSrc.push(getCspNonce) + directives.connectSrc.push(getCspWebSocketUrl) if (config.csp.upgradeInsecureRequests === 'auto') { directives.upgradeInsecureRequests = config.usessl === 'true' } else { -- cgit v1.2.3 From 91101c856c3efac53e8a4db4cc537b77370aa7df Mon Sep 17 00:00:00 2001 From: Literallie Date: Fri, 20 Oct 2017 12:31:16 +0200 Subject: Change CSP config format to be more intuitive --- app.js | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) (limited to 'app.js') diff --git a/app.js b/app.js index 15c9e61e..01ecc84c 100644 --- a/app.js +++ b/app.js @@ -125,7 +125,28 @@ function getCspWebSocketUrl (req, res) { return (req.protocol === 'http' ? 'ws:' : 'wss:') + config.serverurl.replace(/https?:/, "") } +function mergeWithDefaults(configured, defaultDirective, cdnDirective) { + var directive = [].concat(configured) + if (config.csp.addDefaults && defaultDirective) { + directive = directive.concat(defaultDirective) + } + if (config.usecdn && cdnDirective) { + directive = directive.concat(cdnDirective) + } + return directive +} + if (config.csp.enable) { + var defaultDirectives = { + defaultSrc: ['\'self\''], + scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', 'https://*.disqus.com', '\'unsafe-eval\''], // TODO: Remove unsafe-eval - webpack script-loader issues + imgSrc: ['*'], + styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://assets-cdn.github.com'], // unsafe-inline is required for some libs, plus used in views + fontSrc: ['\'self\'', 'https://public.slidesharecdn.com'], + objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/ + childSrc: ['*'], + connectSrc: ['\'self\'', 'https://links.services.disqus.com', 'wss://realtime.services.disqus.com'] + }; var cdnDirectives = { scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], styleSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com'], @@ -134,11 +155,20 @@ if (config.csp.enable) { var directives = {} for (var propertyName in config.csp.directives) { if (config.csp.directives.hasOwnProperty(propertyName)) { - var directive = [].concat(config.csp.directives[propertyName]) - if (config.usecdn && !!cdnDirectives[propertyName]) { - directive = directive.concat(cdnDirectives[propertyName]) - } - directives[propertyName] = directive + directives[propertyName] = mergeWithDefaults( + config.csp.directives[propertyName], + defaultDirectives[propertyName], + cdnDirectives[propertyName] + ) + } + } + for (var propertyName in defaultDirectives) { + if (!directives[propertyName]) { + directives[propertyName] = mergeWithDefaults( + [], + defaultDirectives[propertyName], + cdnDirectives[propertyName] + ) } } directives.scriptSrc.push(getCspNonce) -- cgit v1.2.3 From d51da8c12c2446d081eaa7f32406941b09142c1c Mon Sep 17 00:00:00 2001 From: Literallie Date: Sat, 21 Oct 2017 00:46:53 +0200 Subject: Don't add nonce to CSP if unsafe-inline is on Browsers ignore unsafe-inline if a nonce is sent --- app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app.js') diff --git a/app.js b/app.js index 01ecc84c..8664707d 100644 --- a/app.js +++ b/app.js @@ -171,7 +171,9 @@ if (config.csp.enable) { ) } } - directives.scriptSrc.push(getCspNonce) + if (directives.scriptSrc.indexOf('\'unsafe-inline\'') === -1) { + directives.scriptSrc.push(getCspNonce) + } directives.connectSrc.push(getCspWebSocketUrl) if (config.csp.upgradeInsecureRequests === 'auto') { directives.upgradeInsecureRequests = config.usessl === 'true' -- cgit v1.2.3 From 2b2b8d6d1daa0cd8a90459a9592d2c0dd753f8b2 Mon Sep 17 00:00:00 2001 From: Literallie Date: Sat, 21 Oct 2017 00:48:48 +0200 Subject: Allow any connect-src in CSP Managing these for all the integrations seems like a lot of effort --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app.js') diff --git a/app.js b/app.js index 8664707d..c382cc35 100644 --- a/app.js +++ b/app.js @@ -145,7 +145,7 @@ if (config.csp.enable) { fontSrc: ['\'self\'', 'https://public.slidesharecdn.com'], objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/ childSrc: ['*'], - connectSrc: ['\'self\'', 'https://links.services.disqus.com', 'wss://realtime.services.disqus.com'] + connectSrc: ['*'] }; var cdnDirectives = { scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], -- cgit v1.2.3 From e5f03fe1356dca679b6e19c0dc02136f8f78936a Mon Sep 17 00:00:00 2001 From: Literallie Date: Sat, 21 Oct 2017 01:25:30 +0200 Subject: Add dirty workaround for speakers view inline script --- app.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app.js') diff --git a/app.js b/app.js index c382cc35..cdabc7d7 100644 --- a/app.js +++ b/app.js @@ -173,6 +173,10 @@ if (config.csp.enable) { } if (directives.scriptSrc.indexOf('\'unsafe-inline\'') === -1) { directives.scriptSrc.push(getCspNonce) + // TODO: This is the SHA-256 hash of the inline script in + // build/reveal.js/plugins/notes/notes.html . Any cleaner + // solution appreciated. + directives.scriptSrc.push('\'sha256-EtvSSxRwce5cLeFBZbvZvDrTiRoyoXbWWwvEVciM5Ag=\'') } directives.connectSrc.push(getCspWebSocketUrl) if (config.csp.upgradeInsecureRequests === 'auto') { -- cgit v1.2.3 From 04f5e3a3414abbb76841df8375598fb690323f11 Mon Sep 17 00:00:00 2001 From: Literallie Date: Sun, 22 Oct 2017 01:22:48 +0200 Subject: Move CSP logic to new file, Fix boolean config examples Not sure why I was quoting these in the first place --- app.js | 77 ++++-------------------------------------------------------------- 1 file changed, 4 insertions(+), 73 deletions(-) (limited to 'app.js') diff --git a/app.js b/app.js index cdabc7d7..055b8f4c 100644 --- a/app.js +++ b/app.js @@ -12,7 +12,6 @@ var session = require('express-session') var SequelizeStore = require('connect-session-sequelize')(session.Store) var fs = require('fs') var path = require('path') -var uuid = require('uuid') var morgan = require('morgan') var passportSocketIo = require('passport.socketio') @@ -25,6 +24,7 @@ var config = require('./lib/config') var logger = require('./lib/logger') var response = require('./lib/response') var models = require('./lib/models') +var csp = require('./lib/csp') // generate front-end constants by template var constpath = path.join(__dirname, './public/js/lib/common/constant.ejs') @@ -109,83 +109,14 @@ if (config.hsts.enable) { logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security') } -app.use((req, res, next) => { - res.locals.nonce = uuid.v4() - next() -}) +// Generate a random nonce per request, for CSP with inline scripts +app.use(csp.addNonceToLocals) // use Content-Security-Policy to limit XSS, dangerous plugins, etc. // https://helmetjs.github.io/docs/csp/ -function getCspNonce (req, res) { - return "'nonce-" + res.locals.nonce + "'" -} - -function getCspWebSocketUrl (req, res) { - // wss: is included in 'self', but 'ws:' is not - return (req.protocol === 'http' ? 'ws:' : 'wss:') + config.serverurl.replace(/https?:/, "") -} - -function mergeWithDefaults(configured, defaultDirective, cdnDirective) { - var directive = [].concat(configured) - if (config.csp.addDefaults && defaultDirective) { - directive = directive.concat(defaultDirective) - } - if (config.usecdn && cdnDirective) { - directive = directive.concat(cdnDirective) - } - return directive -} - if (config.csp.enable) { - var defaultDirectives = { - defaultSrc: ['\'self\''], - scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', 'https://*.disqus.com', '\'unsafe-eval\''], // TODO: Remove unsafe-eval - webpack script-loader issues - imgSrc: ['*'], - styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://assets-cdn.github.com'], // unsafe-inline is required for some libs, plus used in views - fontSrc: ['\'self\'', 'https://public.slidesharecdn.com'], - objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/ - childSrc: ['*'], - connectSrc: ['*'] - }; - var cdnDirectives = { - scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], - styleSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com'], - fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com'] - } - var directives = {} - for (var propertyName in config.csp.directives) { - if (config.csp.directives.hasOwnProperty(propertyName)) { - directives[propertyName] = mergeWithDefaults( - config.csp.directives[propertyName], - defaultDirectives[propertyName], - cdnDirectives[propertyName] - ) - } - } - for (var propertyName in defaultDirectives) { - if (!directives[propertyName]) { - directives[propertyName] = mergeWithDefaults( - [], - defaultDirectives[propertyName], - cdnDirectives[propertyName] - ) - } - } - if (directives.scriptSrc.indexOf('\'unsafe-inline\'') === -1) { - directives.scriptSrc.push(getCspNonce) - // TODO: This is the SHA-256 hash of the inline script in - // build/reveal.js/plugins/notes/notes.html . Any cleaner - // solution appreciated. - directives.scriptSrc.push('\'sha256-EtvSSxRwce5cLeFBZbvZvDrTiRoyoXbWWwvEVciM5Ag=\'') - } - directives.connectSrc.push(getCspWebSocketUrl) - if (config.csp.upgradeInsecureRequests === 'auto') { - directives.upgradeInsecureRequests = config.usessl === 'true' - } else { - directives.upgradeInsecureRequests = config.csp.upgradeInsecureRequests === 'true' - } app.use(helmet.contentSecurityPolicy({ - directives: directives + directives: csp.computeDirectives() })) } else { logger.info('Content-Security-Policy is disabled. This may be a security risk.') -- cgit v1.2.3