diff --git a/docs/content/configuration.md b/docs/content/configuration.md index c6e3fc3eb..51f82581a 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -91,6 +91,8 @@ these are rarely used for various reasons. | `csp.addGoogleAnalytics` | `CMD_CSP_ADD_GOOGLE_ANALYTICS` | **`false`** or `true` | Enable to allow users to add Google Analytics to their notes. We don't recommend enabling this option, as it increases the attack surface of XSS attacks. | | `csp.upgradeInsecureRequests` | | **`auto`** or `true` or `false` | By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set to either `true` or `false`. | | `csp.reportUri` | `CMD_CSP_REPORTURI` | **`undefined`**, `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations. | +| `csp.allowFraming` | `CMD_CSP_ALLOW_FRAMING` | **`true`** or `false` | Disable to disallow embedding of the instance via iframe. We **strongly recommend disabling** this option, as it increases the attack surface of XSS attacks. | +| `csp.allowPDFEmbed` | `CMD_CSP_ALLOW_PDF_EMBED` | **`true`** or `false` | Disable to disallow embedding PDFs. We recommend disabling this option, as it increases the attack surface of XSS attacks. | | `cookiePolicy` | `CMD_COOKIE_POLICY` | **`lax`**, `strict` or `none` | Set a SameSite policy whether cookies are send from cross-origin. Be careful: setting a SameSite value of none without https breaks the editor. | ## Privacy and External Requests diff --git a/lib/config/default.js b/lib/config/default.js index c1f3f9733..c687e4841 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -25,7 +25,9 @@ module.exports = { addDisqus: false, addGoogleAnalytics: false, upgradeInsecureRequests: 'auto', - reportURI: undefined + reportURI: undefined, + allowFraming: true, + allowPDFEmbed: true }, cookiePolicy: 'lax', protocolUseSSL: false, diff --git a/lib/config/environment.js b/lib/config/environment.js index 1a43a88f9..cd83dc12f 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -22,7 +22,9 @@ module.exports = { enable: toBooleanConfig(process.env.CMD_CSP_ENABLE), reportURI: process.env.CMD_CSP_REPORTURI, addDisqus: toBooleanConfig(process.env.CMD_CSP_ADD_DISQUS), - addGoogleAnalytics: toBooleanConfig(process.env.CMD_CSP_ADD_GOOGLE_ANALYTICS) + addGoogleAnalytics: toBooleanConfig(process.env.CMD_CSP_ADD_GOOGLE_ANALYTICS), + allowFraming: toBooleanConfig(process.env.CMD_CSP_ALLOW_FRAMING), + allowPDFEmbed: toBooleanConfig(process.env.CMD_CSP_ALLOW_PDF_EMBED) }, cookiePolicy: process.env.CMD_COOKIE_POLICY, protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL), diff --git a/lib/csp.js b/lib/csp.js index 74404413c..52a8d4b8a 100644 --- a/lib/csp.js +++ b/lib/csp.js @@ -4,15 +4,26 @@ const { v4: uuidv4 } = require('uuid') const CspStrategy = {} const defaultDirectives = { - defaultSrc: ['\'self\''], - scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net'], - imgSrc: ['*'], - styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views - fontSrc: ['\'self\'', 'data:', 'https://public.slidesharecdn.com'], + defaultSrc: ['\'none\''], + baseUri: ['\'self\''], + connectSrc: ['\'self\''], + fontSrc: ['\'self\''], + manifestSrc: ['\'self\''], + frameSrc: ['\'self\'', 'https://player.vimeo.com', 'https://www.slideshare.net/slideshow/embed_code/key/', 'https://www.youtube.com'], + imgSrc: ['*'], // we allow using arbitrary images + scriptSrc: [ + config.serverURL + '/build/', + config.serverURL + '/js/', + config.serverURL + '/config', + 'https://gist.github.com/', + 'https://vimeo.com/api/oembed.json', + 'https://www.slideshare.net/api/oembed/2', + '\'unsafe-inline\'' // this is ignored by browsers supporting nonces/hashes + ], + styleSrc: [config.serverURL + '/build/', config.serverURL + '/css/', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/ - mediaSrc: ['*'], - childSrc: ['*'], - connectSrc: ['*'] + formAction: ['\'self\''], + mediaSrc: ['*'] } const cdnDirectives = { @@ -35,6 +46,15 @@ const dropboxDirectives = { scriptSrc: ['https://www.dropbox.com', '\'unsafe-inline\''] } +const disallowFramingDirectives = { + frameAncestors: ['\'self\''] +} + +const allowPDFEmbedDirectives = { + objectSrc: ['*'], // Chrome and Firefox treat PDFs as objects + frameSrc: ['*'] // Chrome also checks PDFs against frame-src +} + CspStrategy.computeDirectives = function () { const directives = {} mergeDirectives(directives, config.csp.directives) @@ -43,9 +63,9 @@ CspStrategy.computeDirectives = function () { mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives) mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives) mergeDirectivesIf(config.dropbox.appKey, directives, dropboxDirectives) - if (!areAllInlineScriptsAllowed(directives)) { - addInlineScriptExceptions(directives) - } + mergeDirectivesIf(!config.csp.allowFraming, directives, disallowFramingDirectives) + mergeDirectivesIf(config.csp.allowPDFEmbed, directives, allowPDFEmbedDirectives) + addInlineScriptExceptions(directives) addUpgradeUnsafeRequestsOptionTo(directives) addReportURI(directives) return directives @@ -67,10 +87,6 @@ function mergeDirectivesIf (condition, 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 @@ -79,11 +95,11 @@ function addInlineScriptExceptions (directives) { } function getCspNonce (req, res) { - return "'nonce-" + res.locals.nonce + "'" + return '\'nonce-' + res.locals.nonce + '\'' } function addUpgradeUnsafeRequestsOptionTo (directives) { - if (config.csp.upgradeInsecureRequests === 'auto' && config.useSSL) { + if (config.csp.upgradeInsecureRequests === 'auto' && (config.useSSL || config.protocolUseSSL)) { directives.upgradeInsecureRequests = [] } else if (config.csp.upgradeInsecureRequests === true) { directives.upgradeInsecureRequests = [] diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md index 7134a3d92..0f40968d9 100644 --- a/public/docs/release-notes.md +++ b/public/docs/release-notes.md @@ -8,6 +8,11 @@ ### Features - HedgeDoc now automatically retries connecting to the database up to 30 times on startup. +- This release introduces the `csp.allowFraming` config option, which controls whether embedding a HedgeDoc instance + in other webpages is allowed. We **strongly recommend disabling** this option to reduce the risk of XSS attacks. +- This release introduces the `csp.allowPDFEmbed` config option, which controls whether embedding PDFs inside HedgeDoc + notes is allowed. We recommend disabling this option if you don't use the feature, to reduce the attack surface of + XSS attacks. ### Bugfixes - Fix crash when trying to read the current Git commit on startup diff --git a/public/js/extra.js b/public/js/extra.js index 6e3b0ed0e..616d43a32 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -284,12 +284,12 @@ export function finishView (view) { // youtube view.find('div.youtube.raw').removeClass('raw') .click(function () { - imgPlayiframe(this, '//www.youtube.com/embed/') + imgPlayiframe(this, 'https://www.youtube.com/embed/') }) // vimeo view.find('div.vimeo.raw').removeClass('raw') .click(function () { - imgPlayiframe(this, '//player.vimeo.com/video/') + imgPlayiframe(this, 'https://player.vimeo.com/video/') }) .each((key, value) => { const vimeoLink = `https://vimeo.com/${$(value).attr('data-videoid')}` @@ -453,7 +453,7 @@ export function finishView (view) { .each((key, value) => { $.ajax({ type: 'GET', - url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`, + url: `https://www.slideshare.net/api/oembed/2?url=https://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`, jsonp: 'callback', dataType: 'jsonp', success (data) { @@ -1118,7 +1118,7 @@ const youtubePlugin = new Plugin( if (!videoid) return const div = $('
') div.attr('data-videoid', videoid) - const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg` + const thumbnailSrc = `https://img.youtube.com/vi/${videoid}/hqdefault.jpg` const image = `` div.append(image) const icon = '' diff --git a/test/csp.js b/test/csp.js index 154120221..9257eed96 100644 --- a/test/csp.js +++ b/test/csp.js @@ -144,7 +144,7 @@ describe('Content security policies', function () { const variations = ['default', 'script', 'img', 'style', 'font', 'object', 'media', 'child', 'connect'] for (let i = 0; i < variations.length; i++) { - assert.strictEqual(csp.computeDirectives()[variations[i] + 'Src'].toString(), ['https://' + variations[i] + '.example.com'].concat(unextendedCSP[variations[i] + 'Src']).toString()) + assert.strictEqual(csp.computeDirectives()[variations[i] + 'Src'].toString(), ['https://' + variations[i] + '.example.com'].concat(unextendedCSP[variations[i] + 'Src']).filter(x => x != null).toString()) } })