From 832fdd7cb6a3f00116007e0ada5bda10b0a448f8 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/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 ++- 5 files changed, 86 insertions(+), 61 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index f000d7068422a95..547dd6f379db344 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 people 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 7ffa052d47797e1..c0c00acc5367402 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 f25e4db3ec35f45..7c41450c6489948 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 06d993eb3a03df0..615a0dc669fc792 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 e7f30318072c95d..10ae572da6f9926 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)