From e413f0561a5cb3c8bc61c5403568305ce0a73e83 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 1 Feb 2019 13:17:37 -0500 Subject: [PATCH] csp: optional strict mode to block insecure browsers A content security policy is a great addition to the protections built into Kibana, but it's not effective in older browsers (like IE11) that do not enforce the policy. When CSP strict mode is enabled, right before the Kibana app is bootstrapped, a basic safety check is performed to see if "naked" inline scripts are rejected. If inline scripting is allowed by the browser, then an error message is presented to the user and Kibana never attempts to bootstrap. --- docs/setup/production.asciidoc | 20 ++++ docs/setup/settings.asciidoc | 2 + src/server/config/schema.js | 1 + src/ui/ui_render/bootstrap/template.js.hbs | 126 +++++++++++---------- src/ui/ui_render/ui_render_mixin.js | 1 + src/ui/ui_render/views/ui_app.pug | 17 ++- 6 files changed, 106 insertions(+), 61 deletions(-) diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index 94b1e1fb339fc4..2297e53a0bebe0 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -2,6 +2,7 @@ == Using Kibana in a production environment * <> +* <> * <> * <> @@ -36,6 +37,25 @@ which users can load which dashboards. For information about setting up Kibana users, see {kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana]. +[float] +[[csp-strict-mode]] +=== Require Content Security Policy + +Kibana uses a Content Security Policy to help prevent the browser from allowing +unsafe scripting, but older browsers will silently ignore this policy. If your +organization does not need to support Internet Explorer 11 or much older +versions of our other supported browsers, we recommend that you enable Kibana's +`strict` mode for content security policy, which will block access to Kibana +for any browser that does not enforce even a rudimentary set of CSP +protections. + +To do this, set `csp.strict` to `true` in your `kibana.yml`: + +-------- +csp.strict: true +-------- + + [float] [[enabling-ssl]] === Enabling SSL diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 07a2cd07700888..3fbb98183ba7b7 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -21,6 +21,8 @@ you'll need to update your `kibana.yml` file. You can also enable SSL and set a `csp.rules:`:: A template https://w3c.github.io/webappsec-csp/[content-security-policy] that disables certain unnecessary and potentially insecure capabilities in the browser. All instances of `{nonce}` will be replaced with an automatically generated nonce at load time. We strongly recommend that you keep the default CSP rules that ship with Kibana. +`csp.strict:`:: *Default: `false`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable support for older, less safe browsers like Internet Explorer. + `elasticsearch.customHeaders:`:: *Default: `{}`* Header names and values to send to Elasticsearch. Any custom headers cannot be overwritten by client-side headers, regardless of the `elasticsearch.requestHeadersWhitelist` configuration. diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 7ffa052d47797e..c0c00acc536740 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -97,6 +97,7 @@ export default () => Joi.object({ csp: Joi.object({ rules: Joi.array().items(Joi.string()).default(DEFAULT_CSP_RULES), + strict: Joi.boolean().default(false), }).default(), cpu: Joi.object({ diff --git a/src/ui/ui_render/bootstrap/template.js.hbs b/src/ui/ui_render/bootstrap/template.js.hbs index f25e4db3ec35f4..7c41450c648994 100644 --- a/src/ui/ui_render/bootstrap/template.js.hbs +++ b/src/ui/ui_render/bootstrap/template.js.hbs @@ -1,59 +1,67 @@ -window.onload = function () { - var files = [ - '{{dllBundlePath}}/vendors.bundle.dll.js', - '{{regularBundlePath}}/commons.bundle.js', - '{{regularBundlePath}}/{{appId}}.bundle.js' - ]; - - var failure = function () { - // make subsequent calls to failure() noop - failure = function () {}; - - var err = document.createElement('h1'); - err.style['color'] = 'white'; - err.style['font-family'] = 'monospace'; - err.style['text-align'] = 'center'; - err.style['background'] = '#F44336'; - err.style['padding'] = '25px'; - err.innerText = document.querySelector('[data-error-message]').dataset.errorMessage; - - document.body.innerHTML = err.outerHTML; - } - - function loadStyleSheet(path) { - var dom = document.createElement('link'); - - dom.addEventListener('error', failure); - dom.setAttribute('rel', 'stylesheet'); - dom.setAttribute('href', path); - document.head.appendChild(dom); - } - - function createJavascriptElement(path) { - var dom = document.createElement('script'); - - dom.setAttribute('async', ''); - dom.addEventListener('error', failure); - dom.setAttribute('src', file); - dom.addEventListener('load', next); - document.head.appendChild(dom); - } - - {{#each styleSheetPaths}} - loadStyleSheet('{{this}}'); - {{/each}} - - (function next() { - var file = files.shift(); - if (!file) return; - - var dom = document.createElement('script'); - - dom.setAttribute('async', ''); - dom.setAttribute('nonce', window.__webpack_nonce__); - dom.addEventListener('error', failure); - dom.setAttribute('src', file); - dom.addEventListener('load', next); - document.head.appendChild(dom); - }()); -}; +if (window.__kbnStrictCsp && window.__kbnCspNotEnforced__) { + var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); + legacyBrowserError.style = 'display: flex;' +} else { + var loadingMessage = document.getElementById('kbn_loading_message'); + loadingMessage.style = 'display: flex;' + + window.onload = function () { + var files = [ + '{{dllBundlePath}}/vendors.bundle.dll.js', + '{{regularBundlePath}}/commons.bundle.js', + '{{regularBundlePath}}/{{appId}}.bundle.js' + ]; + + var failure = function () { + // make subsequent calls to failure() noop + failure = function () {}; + + var err = document.createElement('h1'); + err.style['color'] = 'white'; + err.style['font-family'] = 'monospace'; + err.style['text-align'] = 'center'; + err.style['background'] = '#F44336'; + err.style['padding'] = '25px'; + err.innerText = document.querySelector('[data-error-message]').dataset.errorMessage; + + document.body.innerHTML = err.outerHTML; + } + + function loadStyleSheet(path) { + var dom = document.createElement('link'); + + dom.addEventListener('error', failure); + dom.setAttribute('rel', 'stylesheet'); + dom.setAttribute('href', path); + document.head.appendChild(dom); + } + + function createJavascriptElement(path) { + var dom = document.createElement('script'); + + dom.setAttribute('async', ''); + dom.addEventListener('error', failure); + dom.setAttribute('src', file); + dom.addEventListener('load', next); + document.head.appendChild(dom); + } + + {{#each styleSheetPaths}} + loadStyleSheet('{{this}}'); + {{/each}} + + (function next() { + var file = files.shift(); + if (!file) return; + + var dom = document.createElement('script'); + + dom.setAttribute('async', ''); + dom.setAttribute('nonce', window.__webpack_nonce__); + dom.addEventListener('error', failure); + dom.setAttribute('src', file); + dom.addEventListener('load', next); + document.head.appendChild(dom); + }()); + }; +} diff --git a/src/ui/ui_render/ui_render_mixin.js b/src/ui/ui_render/ui_render_mixin.js index 410aa3a6743f94..70fd67f2925064 100644 --- a/src/ui/ui_render/ui_render_mixin.js +++ b/src/ui/ui_render/ui_render_mixin.js @@ -217,6 +217,7 @@ export function uiRenderMixin(kbnServer, server, config) { const response = h.view('ui_app', { nonce, + strictCsp: config.get('csp.strict'), uiPublicUrl: `${basePath}/ui`, bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`, i18n: (id, options) => i18n.translate(id, options), diff --git a/src/ui/ui_render/views/ui_app.pug b/src/ui/ui_render/views/ui_app.pug index e7f30318072c95..10ae572da6f992 100644 --- a/src/ui/ui_render/views/ui_app.pug +++ b/src/ui/ui_render/views/ui_app.pug @@ -101,8 +101,7 @@ block content } } - - .kibanaWelcomeView + .kibanaWelcomeView(id="kbn_loading_message", style="display: none;") .kibanaLoaderWrap .kibanaLoader .kibanaWelcomeLogoCircle @@ -110,6 +109,20 @@ block content .kibanaWelcomeText(data-error-message=i18n('common.ui.welcomeErrorMessage', { defaultMessage: 'Kibana did not load properly. Check the server output for more information.' })) | #{i18n('common.ui.welcomeMessage', { defaultMessage: 'Loading Kibana' })} + .kibanaWelcomeView(id="kbn_legacy_browser_error", style="display: none;") + .kibanaLoaderWrap + .kibanaWelcomeLogoCircle + .kibanaWelcomeLogo + .kibanaWelcomeText + | #{i18n('common.ui.legacyBrowserMessage', { defaultMessage: 'Your browser version does not meet the required security capabilities of this application.' })} + + script. + // Since this script tag does not contain a nonce, this code will not run + // in browsers that support content security policy(CSP). This is + // intentional as we check for the existence of __kbnCspNotEnforced__ in + // bootstrap. + window.__kbnCspNotEnforced__ = true; script(nonce=nonce). + window.__kbnStrictCsp = !{strictCsp}; window.__webpack_nonce__ = '!{nonce}'; script(src=bootstrapScriptUrl, nonce=nonce)