Merge pull request #1513 from hedgedoc/fix/csp

This commit is contained in:
David Mehren 2021-08-15 00:39:38 +02:00 committed by GitHub
commit fc1aec6cb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 51 additions and 24 deletions

View File

@ -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.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.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://<someid>.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations. | | `csp.reportUri` | `CMD_CSP_REPORTURI` | **`undefined`**, `https://<someid>.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. | | `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 ## Privacy and External Requests

View File

@ -25,7 +25,9 @@ module.exports = {
addDisqus: false, addDisqus: false,
addGoogleAnalytics: false, addGoogleAnalytics: false,
upgradeInsecureRequests: 'auto', upgradeInsecureRequests: 'auto',
reportURI: undefined reportURI: undefined,
allowFraming: true,
allowPDFEmbed: true
}, },
cookiePolicy: 'lax', cookiePolicy: 'lax',
protocolUseSSL: false, protocolUseSSL: false,

View File

@ -22,7 +22,9 @@ module.exports = {
enable: toBooleanConfig(process.env.CMD_CSP_ENABLE), enable: toBooleanConfig(process.env.CMD_CSP_ENABLE),
reportURI: process.env.CMD_CSP_REPORTURI, reportURI: process.env.CMD_CSP_REPORTURI,
addDisqus: toBooleanConfig(process.env.CMD_CSP_ADD_DISQUS), 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, cookiePolicy: process.env.CMD_COOKIE_POLICY,
protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL), protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL),

View File

@ -4,15 +4,26 @@ const { v4: uuidv4 } = require('uuid')
const CspStrategy = {} const CspStrategy = {}
const defaultDirectives = { const defaultDirectives = {
defaultSrc: ['\'self\''], defaultSrc: ['\'none\''],
scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net'], baseUri: ['\'self\''],
imgSrc: ['*'], connectSrc: ['\'self\''],
styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views fontSrc: ['\'self\''],
fontSrc: ['\'self\'', 'data:', 'https://public.slidesharecdn.com'], 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 :/ objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/
mediaSrc: ['*'], formAction: ['\'self\''],
childSrc: ['*'], mediaSrc: ['*']
connectSrc: ['*']
} }
const cdnDirectives = { const cdnDirectives = {
@ -35,6 +46,15 @@ const dropboxDirectives = {
scriptSrc: ['https://www.dropbox.com', '\'unsafe-inline\''] 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 () { CspStrategy.computeDirectives = function () {
const directives = {} const directives = {}
mergeDirectives(directives, config.csp.directives) mergeDirectives(directives, config.csp.directives)
@ -43,9 +63,9 @@ CspStrategy.computeDirectives = function () {
mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives) mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives)
mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives) mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives)
mergeDirectivesIf(config.dropbox.appKey, directives, dropboxDirectives) mergeDirectivesIf(config.dropbox.appKey, directives, dropboxDirectives)
if (!areAllInlineScriptsAllowed(directives)) { mergeDirectivesIf(!config.csp.allowFraming, directives, disallowFramingDirectives)
addInlineScriptExceptions(directives) mergeDirectivesIf(config.csp.allowPDFEmbed, directives, allowPDFEmbedDirectives)
} addInlineScriptExceptions(directives)
addUpgradeUnsafeRequestsOptionTo(directives) addUpgradeUnsafeRequestsOptionTo(directives)
addReportURI(directives) addReportURI(directives)
return 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) { function addInlineScriptExceptions (directives) {
directives.scriptSrc.push(getCspNonce) directives.scriptSrc.push(getCspNonce)
// TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html // 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) { function getCspNonce (req, res) {
return "'nonce-" + res.locals.nonce + "'" return '\'nonce-' + res.locals.nonce + '\''
} }
function addUpgradeUnsafeRequestsOptionTo (directives) { function addUpgradeUnsafeRequestsOptionTo (directives) {
if (config.csp.upgradeInsecureRequests === 'auto' && config.useSSL) { if (config.csp.upgradeInsecureRequests === 'auto' && (config.useSSL || config.protocolUseSSL)) {
directives.upgradeInsecureRequests = [] directives.upgradeInsecureRequests = []
} else if (config.csp.upgradeInsecureRequests === true) { } else if (config.csp.upgradeInsecureRequests === true) {
directives.upgradeInsecureRequests = [] directives.upgradeInsecureRequests = []

View File

@ -8,6 +8,11 @@
### Features ### Features
- HedgeDoc now automatically retries connecting to the database up to 30 times on startup. - 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 ### Bugfixes
- Fix crash when trying to read the current Git commit on startup - Fix crash when trying to read the current Git commit on startup

View File

@ -284,12 +284,12 @@ export function finishView (view) {
// youtube // youtube
view.find('div.youtube.raw').removeClass('raw') view.find('div.youtube.raw').removeClass('raw')
.click(function () { .click(function () {
imgPlayiframe(this, '//www.youtube.com/embed/') imgPlayiframe(this, 'https://www.youtube.com/embed/')
}) })
// vimeo // vimeo
view.find('div.vimeo.raw').removeClass('raw') view.find('div.vimeo.raw').removeClass('raw')
.click(function () { .click(function () {
imgPlayiframe(this, '//player.vimeo.com/video/') imgPlayiframe(this, 'https://player.vimeo.com/video/')
}) })
.each((key, value) => { .each((key, value) => {
const vimeoLink = `https://vimeo.com/${$(value).attr('data-videoid')}` const vimeoLink = `https://vimeo.com/${$(value).attr('data-videoid')}`
@ -453,7 +453,7 @@ export function finishView (view) {
.each((key, value) => { .each((key, value) => {
$.ajax({ $.ajax({
type: 'GET', 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', jsonp: 'callback',
dataType: 'jsonp', dataType: 'jsonp',
success (data) { success (data) {
@ -1118,7 +1118,7 @@ const youtubePlugin = new Plugin(
if (!videoid) return if (!videoid) return
const div = $('<div class="youtube raw"></div>') const div = $('<div class="youtube raw"></div>')
div.attr('data-videoid', videoid) 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 = `<img src="${thumbnailSrc}" />` const image = `<img src="${thumbnailSrc}" />`
div.append(image) div.append(image)
const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>' const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'

View File

@ -144,7 +144,7 @@ describe('Content security policies', function () {
const variations = ['default', 'script', 'img', 'style', 'font', 'object', 'media', 'child', 'connect'] const variations = ['default', 'script', 'img', 'style', 'font', 'object', 'media', 'child', 'connect']
for (let i = 0; i < variations.length; i++) { 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())
} }
}) })