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 --- public/views/slide.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'public') diff --git a/public/views/slide.ejs b/public/views/slide.ejs index 7ff5016e..c7dd9898 100644 --- a/public/views/slide.ejs +++ b/public/views/slide.ejs @@ -41,7 +41,7 @@ - + <% if(useCDN) { %> diff --git a/public/views/pretty.ejs b/public/views/pretty.ejs index 80d2505c..b2988e39 100644 --- a/public/views/pretty.ejs +++ b/public/views/pretty.ejs @@ -72,9 +72,7 @@ - + <% if(useCDN) { %> diff --git a/public/views/slide.ejs b/public/views/slide.ejs index c7dd9898..269ce044 100644 --- a/public/views/slide.ejs +++ b/public/views/slide.ejs @@ -89,9 +89,7 @@ - + <% if(useCDN) { %> -- cgit v1.3.1 From 5b83deb043296c23ff912a2472703c1f7faddb4b Mon Sep 17 00:00:00 2001 From: Literallie Date: Thu, 19 Oct 2017 22:48:13 +0200 Subject: Load js-url lib using legacy-loader Doesn't use eval, plus no window object access --- package.json | 1 + public/js/history.js | 6 ++++-- public/js/index.js | 7 +++++-- webpackBaseConfig.js | 10 +++++++--- 4 files changed, 17 insertions(+), 7 deletions(-) (limited to 'public') diff --git a/package.json b/package.json index 35fe4f9c..0d9f5019 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "js-yaml": "^3.7.0", "jsdom-nogyp": "^0.8.3", "keymaster": "^1.6.2", + "legacy-loader": "0.0.2", "list.js": "^1.5.0", "lodash": "^4.17.4", "lz-string": "1.4.4", diff --git a/public/js/history.js b/public/js/history.js index e14b80d8..da82fd04 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -12,14 +12,16 @@ import { urlpath } from './lib/config' +var jsUrl = require('js-url') + window.migrateHistoryFromTempCallback = null migrateHistoryFromTemp() function migrateHistoryFromTemp () { - if (window.url('#tempid')) { + if (jsUrl('#tempid')) { $.get(`${serverurl}/temp`, { - tempid: window.url('#tempid') + tempid: jsUrl('#tempid') }) .done(data => { if (data && data.temp) { diff --git a/public/js/index.js b/public/js/index.js index b336af90..25bd1c36 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -3,6 +3,7 @@ modeType, Idle, serverurl, key, gapi, Dropbox, FilePicker ot, MediaUploader, hex2rgb, num_loaded, Visibility */ + require('../vendor/showup/showup') require('../css/index.css') @@ -21,6 +22,8 @@ import _ from 'lodash' import List from 'list.js' +var jsUrl = require('js-url') + import { checkLoginStateChanged, setloginStateChangeEvent @@ -1474,12 +1477,12 @@ $('#gistImportModalConfirm').click(function () { if (!isValidURL(gisturl)) { showMessageModal(' Import from Gist', 'Not a valid URL :(', '', '', false) } else { - var hostname = window.url('hostname', gisturl) + var hostname = jsUrl('hostname', gisturl) if (hostname !== 'gist.github.com') { showMessageModal(' Import from Gist', 'Not a valid Gist URL :(', '', '', false) } else { ui.spinner.show() - $.get('https://api.github.com/gists/' + window.url('-1', gisturl)) + $.get('https://api.github.com/gists/' + jsUrl('-1', gisturl)) .done(function (data) { if (data.files) { var contents = '' diff --git a/webpackBaseConfig.js b/webpackBaseConfig.js index 41a63e7d..71a8f2ef 100644 --- a/webpackBaseConfig.js +++ b/webpackBaseConfig.js @@ -190,7 +190,7 @@ module.exports = { index: [ 'babel-polyfill', 'script!jquery-ui-resizable', - 'script!js-url', + 'js-url', 'expose?filterXSS!xss', 'script!Idle.Js', 'expose?LZString!lz-string', @@ -241,7 +241,7 @@ module.exports = { 'expose?jsyaml!js-yaml', 'script!mermaid', 'expose?moment!moment', - 'script!js-url', + 'js-url', 'script!handlebars', 'expose?hljs!highlight.js', 'expose?emojify!emojify.js', @@ -374,7 +374,8 @@ module.exports = { 'bootstrap-tooltip': path.join(__dirname, 'public/vendor/bootstrap/tooltip.min.js'), 'headjs': path.join(__dirname, 'node_modules/reveal.js/lib/js/head.min.js'), 'reveal-markdown': path.join(__dirname, 'public/js/reveal-markdown.js'), - abcjs: path.join(__dirname, 'public/vendor/abcjs_basic_3.1.1-min.js') + abcjs: path.join(__dirname, 'public/vendor/abcjs_basic_3.1.1-min.js'), + 'js-url': path.join(__dirname, 'node_modules/js-url/url.js') } }, @@ -429,6 +430,9 @@ module.exports = { }, { test: /\.gif(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/gif' + }, { + test: /\/node_modules\/js-url\/url.js/, + loader: 'legacy' }] }, node: { -- cgit v1.3.1 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 --- README.md | 4 +-- app.js | 77 +++------------------------------------------------ config.json.example | 10 +++---- lib/csp.js | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ public/js/index.js | 1 - 5 files changed, 91 insertions(+), 81 deletions(-) create mode 100644 lib/csp.js (limited to 'public') 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') -- cgit v1.3.1 From 567f26f5b9a5ffa0c28fba789ad502c54c4035a7 Mon Sep 17 00:00:00 2001 From: Literallie Date: Sun, 22 Oct 2017 02:48:24 +0200 Subject: Fix MathJax config not being picked up thanks standard --- public/js/mathjax-config-extra.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'public') diff --git a/public/js/mathjax-config-extra.js b/public/js/mathjax-config-extra.js index 54cee794..11ba59c6 100644 --- a/public/js/mathjax-config-extra.js +++ b/public/js/mathjax-config-extra.js @@ -1,4 +1,4 @@ -var MathJax = { +window.MathJax = { messageStyle: 'none', skipStartupTypeset: true, tex2jax: { -- cgit v1.3.1 From 3a752fde5117e800d65e26cbe7b15d65eb5b491e Mon Sep 17 00:00:00 2001 From: Literallie Date: Thu, 2 Nov 2017 17:57:44 +0100 Subject: Revert "Load js-url lib using legacy-loader" Didn't work in Firefox for some reason. `[Script Loader] ReferenceError: module is not defined` This reverts commit 5b83deb043296c23ff912a2472703c1f7faddb4b. --- package.json | 1 - public/js/history.js | 6 ++---- public/js/index.js | 6 ++---- webpackBaseConfig.js | 10 +++------- 4 files changed, 7 insertions(+), 16 deletions(-) (limited to 'public') diff --git a/package.json b/package.json index 0d9f5019..35fe4f9c 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "js-yaml": "^3.7.0", "jsdom-nogyp": "^0.8.3", "keymaster": "^1.6.2", - "legacy-loader": "0.0.2", "list.js": "^1.5.0", "lodash": "^4.17.4", "lz-string": "1.4.4", diff --git a/public/js/history.js b/public/js/history.js index da82fd04..e14b80d8 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -12,16 +12,14 @@ import { urlpath } from './lib/config' -var jsUrl = require('js-url') - window.migrateHistoryFromTempCallback = null migrateHistoryFromTemp() function migrateHistoryFromTemp () { - if (jsUrl('#tempid')) { + if (window.url('#tempid')) { $.get(`${serverurl}/temp`, { - tempid: jsUrl('#tempid') + tempid: window.url('#tempid') }) .done(data => { if (data && data.temp) { diff --git a/public/js/index.js b/public/js/index.js index 56522e9c..b336af90 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -21,8 +21,6 @@ import _ from 'lodash' import List from 'list.js' -var jsUrl = require('js-url') - import { checkLoginStateChanged, setloginStateChangeEvent @@ -1476,12 +1474,12 @@ $('#gistImportModalConfirm').click(function () { if (!isValidURL(gisturl)) { showMessageModal(' Import from Gist', 'Not a valid URL :(', '', '', false) } else { - var hostname = jsUrl('hostname', gisturl) + var hostname = window.url('hostname', gisturl) if (hostname !== 'gist.github.com') { showMessageModal(' Import from Gist', 'Not a valid Gist URL :(', '', '', false) } else { ui.spinner.show() - $.get('https://api.github.com/gists/' + jsUrl('-1', gisturl)) + $.get('https://api.github.com/gists/' + window.url('-1', gisturl)) .done(function (data) { if (data.files) { var contents = '' diff --git a/webpackBaseConfig.js b/webpackBaseConfig.js index 71a8f2ef..41a63e7d 100644 --- a/webpackBaseConfig.js +++ b/webpackBaseConfig.js @@ -190,7 +190,7 @@ module.exports = { index: [ 'babel-polyfill', 'script!jquery-ui-resizable', - 'js-url', + 'script!js-url', 'expose?filterXSS!xss', 'script!Idle.Js', 'expose?LZString!lz-string', @@ -241,7 +241,7 @@ module.exports = { 'expose?jsyaml!js-yaml', 'script!mermaid', 'expose?moment!moment', - 'js-url', + 'script!js-url', 'script!handlebars', 'expose?hljs!highlight.js', 'expose?emojify!emojify.js', @@ -374,8 +374,7 @@ module.exports = { 'bootstrap-tooltip': path.join(__dirname, 'public/vendor/bootstrap/tooltip.min.js'), 'headjs': path.join(__dirname, 'node_modules/reveal.js/lib/js/head.min.js'), 'reveal-markdown': path.join(__dirname, 'public/js/reveal-markdown.js'), - abcjs: path.join(__dirname, 'public/vendor/abcjs_basic_3.1.1-min.js'), - 'js-url': path.join(__dirname, 'node_modules/js-url/url.js') + abcjs: path.join(__dirname, 'public/vendor/abcjs_basic_3.1.1-min.js') } }, @@ -430,9 +429,6 @@ module.exports = { }, { test: /\.gif(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/gif' - }, { - test: /\/node_modules\/js-url\/url.js/, - loader: 'legacy' }] }, node: { -- cgit v1.3.1