diff --git a/README.md b/README.md index d71eb1c5..2633b3b6 100644 --- a/README.md +++ b/README.md @@ -171,8 +171,8 @@ Application settings `config.json` | port | `80` | web app port | | alloworigin | `['localhost']` | domain name whitelist | | usessl | `true` or `false` | set to use ssl server (if true will auto turn on `protocolusessl`) | -| hsts | `{"enable": "true", "maxAgeSeconds": "31536000", "includeSubdomains": "true", "preload": "true"}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | -| csp | `{"enable": "true", "directives": {"scriptSrc": "trustwodthy-scripts.example.com"}, "upgradeInsecureRequests": "auto", "addDefaults": "true"}` | Configures [Content Security Policy](https://helmetjs.github.io/docs/csp/). Directives are directly passed to Helmet, so [their format](https://helmetjs.github.io/docs/csp/) applies. Further, some defaults are added so that the application doesn't break. To disable adding these defaults, set `addDefaults` to `false`. If `usecdn` is on, default CDN locations are allowed too. By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `usessl` is on. To change this behaviour, set `upgradeInsecureRequests` to either `true` or `false`. | +| hsts | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | +| csp | `{"enable": true, "directives": {"scriptSrc": "trustworthy-scripts.example.com"}, "upgradeInsecureRequests": "auto", "addDefaults": true}` | Configures [Content Security Policy](https://helmetjs.github.io/docs/csp/). Directives are passed to Helmet - see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. Some defaults are added to the configured values so that the application doesn't break. To disable this behaviour, set `addDefaults` to `false`. Further, if `usecdn` is on, some CDN locations are allowed too. By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `usessl` is on. To change this behaviour, set `upgradeInsecureRequests` to either `true` or `false`. | | protocolusessl | `true` or `false` | set to use ssl protocol for resources path (only applied when domain is set) | | urladdport | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set) | | usecdn | `true` or `false` | set to use CDN resources or not (default is `true`) | 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.') diff --git a/config.json.example b/config.json.example index 7e4394b0..98658770 100644 --- a/config.json.example +++ b/config.json.example @@ -17,17 +17,17 @@ "production": { "domain": "localhost", "hsts": { - "enable": "true", + "enable": true, "maxAgeSeconds": "31536000", - "includeSubdomains": "true", - "preload": "true" + "includeSubdomains": true, + "preload": true }, csp: { - "enable": "true", + "enable": true, "directives": { }, "upgradeInsecureRequests": "auto" - "addDefaults": "true" + "addDefaults": true }, "db": { "username": "", diff --git a/lib/csp.js b/lib/csp.js new file mode 100644 index 00000000..509bc530 --- /dev/null +++ b/lib/csp.js @@ -0,0 +1,80 @@ +var config = require('./config') +var uuid = require('uuid') + +var CspStrategy = {} + +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 https://github.com/hackmdio/hackmd/issues/594 + 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'] +} + +CspStrategy.computeDirectives = function () { + var directives = {} + mergeDirectives(directives, config.csp.directives) + mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives) + mergeDirectivesIf(config.usecdn, directives, cdnDirectives) + if (!areAllInlineScriptsAllowed(directives)) { + addInlineScriptExceptions(directives) + } + addUpgradeUnsafeRequestsOptionTo(directives) + return directives +} + +function mergeDirectives (existingDirectives, newDirectives) { + for (var propertyName in newDirectives) { + var newDirective = newDirectives[propertyName] + if (newDirective) { + var existingDirective = existingDirectives[propertyName] || [] + existingDirectives[propertyName] = existingDirective.concat(newDirective) + } + } +} + +function mergeDirectivesIf (condition, existingDirectives, newDirectives) { + if (condition) { + mergeDirectives(existingDirectives, newDirectives) + } +} + +function areAllInlineScriptsAllowed (directives) { + return directives.scriptSrc.indexOf('\'unsafe-inline\'') !== -1 +} + +function addInlineScriptExceptions (directives) { + directives.scriptSrc.push(getCspNonce) + // TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html + // Any more clean solution appreciated. + directives.scriptSrc.push('\'sha256-EtvSSxRwce5cLeFBZbvZvDrTiRoyoXbWWwvEVciM5Ag=\'') +} + +function getCspNonce (req, res) { + return "'nonce-" + res.locals.nonce + "'" +} + +function addUpgradeUnsafeRequestsOptionTo (directives) { + if (config.csp.upgradeInsecureRequests === 'auto' && config.usessl) { + directives.upgradeInsecureRequests = true + } else if (config.csp.upgradeInsecureRequests === true) { + directives.upgradeInsecureRequests = true + } +} + +CspStrategy.addNonceToLocals = function (req, res, next) { + res.locals.nonce = uuid.v4() + next() +} + +module.exports = CspStrategy diff --git a/public/js/index.js b/public/js/index.js index 25bd1c36..56522e9c 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -3,7 +3,6 @@ modeType, Idle, serverurl, key, gapi, Dropbox, FilePicker ot, MediaUploader, hex2rgb, num_loaded, Visibility */ - require('../vendor/showup/showup') require('../css/index.css')