From 824f910bfe39b8d789ca3edcfca44b66f603a81c Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Thu, 27 Aug 2020 02:04:49 +0200 Subject: Add config option for cookie SameSite policy Signed-off-by: Erik Michelson --- app.js | 2 +- app.json | 4 ++++ config.json.example | 1 + docs/configuration.md | 25 +++++++++++++------------ lib/config/default.js | 1 + lib/config/environment.js | 1 + lib/config/index.js | 5 +++++ lib/web/statusRouter.js | 3 ++- public/js/index.js | 2 +- public/js/lib/common/constant.ejs | 2 ++ public/js/lib/common/login.js | 4 ++-- public/js/lib/editor/index.js | 16 ++++++++-------- public/js/locale.js | 2 +- 13 files changed, 42 insertions(+), 26 deletions(-) diff --git a/app.js b/app.js index d102e816..236c77b9 100644 --- a/app.js +++ b/app.js @@ -147,7 +147,7 @@ app.use(session({ rolling: true, // reset maxAge on every response cookie: { maxAge: config.sessionLife, - sameSite: 'lax', + sameSite: config.cookiePolicy, // be careful: setting a SameSite value of none without https breaks the editor secure: config.useSSL || config.protocolUseSSL || false }, store: sessionStore diff --git a/app.json b/app.json index f2a2b74c..4d68c0df 100644 --- a/app.json +++ b/app.json @@ -56,6 +56,10 @@ "description": "set to use ssl protocol for resources path (only applied when domain is set)", "required": false }, + "CMD_COOKIE_POLICY": { + "description": "Set whether cookies should be sent cross-origin (SameSite value)", + "required": false + }, "CMD_URL_ADDPORT": { "description": "set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set)", "required": false diff --git a/config.json.example b/config.json.example index 0366c3b2..84ae53bd 100644 --- a/config.json.example +++ b/config.json.example @@ -35,6 +35,7 @@ "addDisqus": true, "addGoogleAnalytics": true }, + "cookiePolicy": "strict", "db": { "username": "", "password": "", diff --git a/docs/configuration.md b/docs/configuration.md index a6f63168..2b3ccad1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -69,18 +69,19 @@ these are rarely used for various reasons. | `urlAddPort` | `CMD_URL_ADDPORT` | **`false`** or `true` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | | `allowOrigin` | `CMD_ALLOW_ORIGIN` | **`['localhost']`**, `['codimd.org']`, `[localhost, codimd.org]` | domain name whitelist (use comma to separate) | -## CSP and HSTS - -| config file | environment | **default** and example value | description | -| ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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) | -| | `CMD_HSTS_ENABLE` | **`true`** or `false` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) | -| | `CMD_HSTS_INCLUDE_SUBDOMAINS` | **`true`** or `false` | set to include subdomains in HSTS (default is `true`) | -| | `CMD_HSTS_MAX_AGE` | **`31536000`**, `60 * 60 * 24 * 365` | max duration in seconds to tell clients to keep HSTS status (default is a year) | -| | `CMD_HSTS_PRELOAD` | **`true`** or `false` | whether to allow preloading of the site's HSTS status (e.g. into browsers) | -| `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`. | -| | `CMD_CSP_ENABLE` | **`true`** or `false` | whether to enable Content Security Policy (directives cannot be configured with environment variables) | -| | `CMD_CSP_REPORTURI` | **`undefined`**, `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations | +## Web security aspects + +| config file | environment | **default** and example value | description | +| -------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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) | +| | `CMD_HSTS_ENABLE` | **`true`** or `false` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) | +| | `CMD_HSTS_INCLUDE_SUBDOMAINS` | **`true`** or `false` | set to include subdomains in HSTS (default is `true`) | +| | `CMD_HSTS_MAX_AGE` | **`31536000`**, `60 * 60 * 24 * 365` | max duration in seconds to tell clients to keep HSTS status (default is a year) | +| | `CMD_HSTS_PRELOAD` | **`true`** or `false` | whether to allow preloading of the site's HSTS status (e.g. into browsers) | +| `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`. | +| | `CMD_CSP_ENABLE` | **`true`** or `false` | whether to enable Content Security Policy (directives cannot be configured with environment variables) | +| | `CMD_CSP_REPORTURI` | **`undefined`**, `https://.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations | +| `cookiePolicy` | `CMD_COOKIE_POLICY` | **`strict`**, `lax` 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 9284882a..3d1a1367 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -27,6 +27,7 @@ module.exports = { upgradeInsecureRequests: 'auto', reportURI: undefined }, + cookiePolicy: 'strict', protocolUseSSL: false, useCDN: false, allowAnonymous: true, diff --git a/lib/config/environment.js b/lib/config/environment.js index 2d76286f..cf9fb5a1 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -22,6 +22,7 @@ module.exports = { enable: toBooleanConfig(process.env.CMD_CSP_ENABLE), reportURI: process.env.CMD_CSP_REPORTURI }, + cookiePolicy: process.env.CMD_COOKIE_POLICY, protocolUseSSL: toBooleanConfig(process.env.CMD_PROTOCOL_USESSL), allowOrigin: toArrayConfig(process.env.CMD_ALLOW_ORIGIN), useCDN: toBooleanConfig(process.env.CMD_USECDN), diff --git a/lib/config/index.js b/lib/config/index.js index ee4817b3..020dee13 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -51,6 +51,11 @@ if (['debug', 'verbose', 'info', 'warn', 'error'].includes(config.loglevel)) { logger.error('Selected loglevel %s doesn\'t exist, using default level \'debug\'. Available options: debug, verbose, info, warn, error', config.loglevel) } +if (!['strict', 'lax', 'none'].includes(config.cookiePolicy)) { + logger.error('Cookie SameSite policy %s does not exist. Falling back to strict. Available values are: strict, lax, none.', config.cookiePolicy) + config.cookiePolicy = 'strict' +} + // load LDAP CA if (config.ldap.tlsca) { let ca = config.ldap.tlsca.split(',') diff --git a/lib/web/statusRouter.js b/lib/web/statusRouter.js index 025aafd4..febe2df3 100644 --- a/lib/web/statusRouter.js +++ b/lib/web/statusRouter.js @@ -97,7 +97,8 @@ statusRouter.get('/config', function (req, res) { version: config.fullversion, DROPBOX_APP_KEY: config.dropbox.appKey, allowedUploadMimeTypes: config.allowedUploadMimeTypes, - linkifyHeaderStyle: config.linkifyHeaderStyle + linkifyHeaderStyle: config.linkifyHeaderStyle, + cookiePolicy: config.cookiePolicy } res.set({ 'Cache-Control': 'private', // only cache by client diff --git a/public/js/index.js b/public/js/index.js index ad20ffff..36f396fa 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1597,7 +1597,7 @@ function toggleNightMode () { } else { Cookies.set('nightMode', !isActive, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) } } diff --git a/public/js/lib/common/constant.ejs b/public/js/lib/common/constant.ejs index 114a9077..2a32c333 100644 --- a/public/js/lib/common/constant.ejs +++ b/public/js/lib/common/constant.ejs @@ -8,3 +8,5 @@ window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %> window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>' window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' + +window.cookiePolicy = '<%- cookiePolicy %>' diff --git a/public/js/lib/common/login.js b/public/js/lib/common/login.js index 931c115f..3f7a3e4d 100644 --- a/public/js/lib/common/login.js +++ b/public/js/lib/common/login.js @@ -20,12 +20,12 @@ export function resetCheckAuth () { export function setLoginState (bool, id) { Cookies.set('loginstate', bool, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) if (id) { Cookies.set('userid', id, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) } else { Cookies.remove('userid') diff --git a/public/js/lib/editor/index.js b/public/js/lib/editor/index.js index 07ef58a1..d86ebf3c 100644 --- a/public/js/lib/editor/index.js +++ b/public/js/lib/editor/index.js @@ -304,13 +304,13 @@ export default class Editor { if (this.editor.getOption('indentWithTabs')) { Cookies.set('indent_type', 'tab', { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) type.text('Tab Size:') } else { Cookies.set('indent_type', 'space', { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) type.text('Spaces:') } @@ -322,12 +322,12 @@ export default class Editor { if (this.editor.getOption('indentWithTabs')) { Cookies.set('tab_size', unit, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) } else { Cookies.set('space_units', unit, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) } widthLabel.text(unit) @@ -396,7 +396,7 @@ export default class Editor { var keymap = this.editor.getOption('keyMap') Cookies.set('keymap', keymap, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) label.text(keymap) this.restoreOverrideEditorKeymap() @@ -445,7 +445,7 @@ export default class Editor { this.editor.setOption('theme', theme) Cookies.set('theme', theme, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) checkTheme() @@ -491,7 +491,7 @@ export default class Editor { } Cookies.set('spellcheck', mode === 'spell-checker', { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) checkSpellcheck() @@ -537,7 +537,7 @@ export default class Editor { if (overrideBrowserKeymap.is(':checked')) { Cookies.set('preferences-override-browser-keymap', true, { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) this.restoreOverrideEditorKeymap() } else { diff --git a/public/js/locale.js b/public/js/locale.js index aca35b98..8baa77fc 100644 --- a/public/js/locale.js +++ b/public/js/locale.js @@ -31,7 +31,7 @@ if (localeSelector.length > 0) { localeSelector.change(function () { Cookies.set('locale', $(this).val(), { expires: 365, - sameSite: 'strict' + sameSite: window.cookiePolicy }) window.location.reload() }) -- cgit v1.2.3