diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 80a7f14c73ec..583e472aa029 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -27,10 +27,8 @@ in a manner that is inconsistent with `/proc/self/cgroup` `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. +certain unnecessary and potentially insecure capabilities in the browser. 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 diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts index 14a82e9404db..caa7b27773bf 100644 --- a/src/core/public/plugins/plugin_loader.test.ts +++ b/src/core/public/plugins/plugin_loader.test.ts @@ -40,14 +40,12 @@ beforeEach(() => { appendChildSpy = jest.spyOn(document.body, 'appendChild').mockReturnValue({} as any); // Mock global fields needed for loading modules. - coreWindow.__kbnNonce__ = 'asdf'; coreWindow.__kbnBundles__ = {}; }); afterEach(() => { appendChildSpy.mockRestore(); createElementSpy.mockRestore(); - delete coreWindow.__kbnNonce__; delete coreWindow.__kbnBundles__; }); @@ -64,7 +62,6 @@ test('`loadPluginBundles` creates a script tag and loads initializer', async () '/bundles/plugin/plugin-a.bundle.js' ); expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('id', 'kbn-plugin-plugin-a'); - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('nonce', 'asdf'); expect(fakeScriptTag.onload).toBeInstanceOf(Function); expect(fakeScriptTag.onerror).toBeInstanceOf(Function); expect(appendChildSpy).toHaveBeenCalledWith(fakeScriptTag); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts index 871091324a6b..776ed7d7c557 100644 --- a/src/core/public/plugins/plugin_loader.ts +++ b/src/core/public/plugins/plugin_loader.ts @@ -31,7 +31,6 @@ export type UnknownPluginInitializer = PluginInitializer { clearTimeout(timeout); // Set to null for IE memory leak issue. Webpack does the same thing. diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js index 3a7814ac6e86..094d4847bbbd 100644 --- a/src/legacy/server/config/transform_deprecations.js +++ b/src/legacy/server/config/transform_deprecations.js @@ -67,6 +67,42 @@ const dataPath = (settings, log) => { } }; +const NONCE_STRING = `{nonce}`; +// Policies that should include the 'self' source +const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); +const SELF_STRING = `'self'`; + +const cspRules = (settings, log) => { + const rules = _.get(settings, 'csp.rules'); + if (!rules) { + return; + } + + const parsed = new Map(rules.map(ruleStr => { + const parts = ruleStr.split(/\s+/); + return [parts[0], parts.slice(1)]; + })); + + settings.csp.rules = [...parsed].map(([policy, sourceList]) => { + if (sourceList.find(source => source.includes(NONCE_STRING))) { + log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); + sourceList = sourceList.filter(source => !source.includes(NONCE_STRING)); + + // Add 'self' if not present + if (!sourceList.find(source => source.includes(SELF_STRING))) { + sourceList.push(SELF_STRING); + } + } + + if (SELF_POLICIES.includes(policy) && !sourceList.find(source => source.includes(SELF_STRING))) { + log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); + sourceList.push(SELF_STRING); + } + + return `${policy} ${sourceList.join(' ')}`.trim(); + }); +}; + const deprecations = [ //server unused('server.xsrf.token'), @@ -80,7 +116,8 @@ const deprecations = [ rewriteBasePath, loggingTimezone, configPath, - dataPath + dataPath, + cspRules ]; export const transformDeprecations = createTransform(deprecations); diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js index 2aaffb3d01e0..38044357f230 100644 --- a/src/legacy/server/config/transform_deprecations.test.js +++ b/src/legacy/server/config/transform_deprecations.test.js @@ -22,13 +22,12 @@ import { transformDeprecations } from './transform_deprecations'; describe('server/config', function () { describe('transformDeprecations', function () { - describe('savedObjects.indexCheckTimeout', () => { it('removes the indexCheckTimeout and savedObjects properties', () => { const settings = { savedObjects: { - indexCheckTimeout: 123 - } + indexCheckTimeout: 123, + }, }; expect(transformDeprecations(settings)).toEqual({}); @@ -38,22 +37,22 @@ describe('server/config', function () { const settings = { savedObjects: { indexCheckTimeout: 123, - foo: 'bar' - } + foo: 'bar', + }, }; expect(transformDeprecations(settings)).toEqual({ savedObjects: { - foo: 'bar' - } + foo: 'bar', + }, }); }); it('logs that the setting is no longer necessary', () => { const settings = { savedObjects: { - indexCheckTimeout: 123 - } + indexCheckTimeout: 123, + }, }; const log = sinon.spy(); @@ -62,5 +61,124 @@ describe('server/config', function () { sinon.assert.calledWithExactly(log, sinon.match('savedObjects.indexCheckTimeout')); }); }); + + describe('csp.rules', () => { + describe('with nonce source', () => { + it('logs a warning', () => { + const settings = { + csp: { + rules: [`script-src 'self' 'nonce-{nonce}'`], + }, + }; + + const log = jest.fn(); + transformDeprecations(settings, log); + expect(log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", + ], + ] + `); + }); + + it('replaces a nonce', () => { + expect( + transformDeprecations( + { csp: { rules: [`script-src 'nonce-{nonce}'`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`]); + expect( + transformDeprecations( + { csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'unsafe-eval' 'self'`]); + }); + + it('removes a quoted nonce', () => { + expect( + transformDeprecations( + { csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`]); + expect( + transformDeprecations( + { csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes a non-quoted nonce', () => { + expect( + transformDeprecations( + { csp: { rules: [`script-src 'self' nonce-{nonce}`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`]); + expect( + transformDeprecations( + { csp: { rules: [`script-src nonce-{nonce} 'self'`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes a strange nonce', () => { + expect( + transformDeprecations( + { csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes multiple nonces', () => { + expect( + transformDeprecations( + { + csp: { + rules: [ + `script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`, + `style-src 'nonce-{nonce}' 'self'`, + ], + }, + }, + jest.fn() + ).csp.rules + ).toEqual([`script-src 'self'`, `style-src 'self'`]); + }); + }); + + describe('without self source', () => { + it('logs a warning', () => { + const log = jest.fn(); + transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, log); + expect(log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "csp.rules must contain the 'self' source. Automatically adding to script-src.", + ], + ] + `); + }); + + it('adds self', () => { + expect( + transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, jest.fn()).csp + .rules + ).toEqual([`script-src 'unsafe-eval' 'self'`]); + }); + }); + + it('does not add self to other policies', () => { + expect( + transformDeprecations({ csp: { rules: [`worker-src blob:`] } }, jest.fn()).csp.rules + ).toEqual([`worker-src blob:`]); + }); + }); }); }); diff --git a/src/legacy/server/csp/index.test.ts b/src/legacy/server/csp/index.test.ts index c11c3cd2d6e1..53c33ecec866 100644 --- a/src/legacy/server/csp/index.test.ts +++ b/src/legacy/server/csp/index.test.ts @@ -19,7 +19,6 @@ import { createCSPRuleString, - generateCSPNonce, DEFAULT_CSP_RULES, DEFAULT_CSP_STRICT, DEFAULT_CSP_WARN_LEGACY_BROWSERS, @@ -40,7 +39,7 @@ import { test('default CSP rules', () => { expect(DEFAULT_CSP_RULES).toMatchInlineSnapshot(` Array [ - "script-src 'unsafe-eval' 'nonce-{nonce}'", + "script-src 'unsafe-eval' 'self'", "worker-src blob:", "child-src blob:", "style-src 'unsafe-inline' 'self'", @@ -56,32 +55,8 @@ test('CSP legacy browser warning defaults to enabled', () => { expect(DEFAULT_CSP_WARN_LEGACY_BROWSERS).toBe(true); }); -test('generateCSPNonce() creates a 16 character string', async () => { - const nonce = await generateCSPNonce(); - - expect(nonce).toHaveLength(16); -}); - -test('generateCSPNonce() creates a new string on each call', async () => { - const nonce1 = await generateCSPNonce(); - const nonce2 = await generateCSPNonce(); - - expect(nonce1).not.toEqual(nonce2); -}); - test('createCSPRuleString() converts an array of rules into a CSP header string', () => { const csp = createCSPRuleString([`string-src 'self'`, 'worker-src blob:', 'img-src data: blob:']); expect(csp).toMatchInlineSnapshot(`"string-src 'self'; worker-src blob:; img-src data: blob:"`); }); - -test('createCSPRuleString() replaces all occurrences of {nonce} if provided', () => { - const csp = createCSPRuleString( - [`string-src 'self' 'nonce-{nonce}'`, 'img-src data: blob:', `default-src 'nonce-{nonce}'`], - 'foo' - ); - - expect(csp).toMatchInlineSnapshot( - `"string-src 'self' 'nonce-foo'; img-src data: blob:; default-src 'nonce-foo'"` - ); -}); diff --git a/src/legacy/server/csp/index.ts b/src/legacy/server/csp/index.ts index d0b626e3fa2c..c48311be75a9 100644 --- a/src/legacy/server/csp/index.ts +++ b/src/legacy/server/csp/index.ts @@ -17,13 +17,8 @@ * under the License. */ -import { randomBytes } from 'crypto'; -import { promisify } from 'util'; - -const randomBytesAsync = promisify(randomBytes); - export const DEFAULT_CSP_RULES = Object.freeze([ - `script-src 'unsafe-eval' 'nonce-{nonce}'`, + `script-src 'unsafe-eval' 'self'`, 'worker-src blob:', 'child-src blob:', `style-src 'unsafe-inline' 'self'`, @@ -33,14 +28,6 @@ export const DEFAULT_CSP_STRICT = false; export const DEFAULT_CSP_WARN_LEGACY_BROWSERS = true; -export async function generateCSPNonce() { - return (await randomBytesAsync(12)).toString('base64'); -} - -export function createCSPRuleString(rules: string[], nonce?: string) { - let ruleString = rules.join('; '); - if (nonce) { - ruleString = ruleString.replace(/\{nonce\}/g, nonce); - } - return ruleString; +export function createCSPRuleString(rules: string[]) { + return rules.join('; '); } diff --git a/src/legacy/ui/ui_bundles/app_entry_template.js b/src/legacy/ui/ui_bundles/app_entry_template.js index 4b63ba5e0961..09ae9c91a518 100644 --- a/src/legacy/ui/ui_bundles/app_entry_template.js +++ b/src/legacy/ui/ui_bundles/app_entry_template.js @@ -26,12 +26,6 @@ export const appEntryTemplate = (bundle) => ` * context: ${bundle.getContext()} */ -// ensure the csp nonce is set in the dll -import 'dll/set_csp_nonce'; - -// set the csp nonce in the primary webpack bundle too -__webpack_nonce__ = window.__kbnNonce__; - // import global polyfills import Symbol_observable from 'symbol-observable'; import '@babel/polyfill'; diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 9f4b175b1860..d30562605754 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,6 +1,5 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; -window.__kbnNonce__ = kbnCsp.nonce; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); @@ -65,7 +64,6 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var dom = document.createElement('script'); dom.setAttribute('async', ''); - dom.setAttribute('nonce', window.__kbnNonce__); dom.addEventListener('error', failure); dom.setAttribute('src', file); dom.addEventListener('load', next); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index d6acff7e8265..47d13184bfd0 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -26,7 +26,7 @@ import { i18n } from '@kbn/i18n'; import { AppBootstrap } from './bootstrap'; import { mergeVariables } from './lib'; import { fromRoot } from '../../utils'; -import { generateCSPNonce, createCSPRuleString } from '../../server/csp'; +import { createCSPRuleString } from '../../server/csp'; export function uiRenderMixin(kbnServer, server, config) { function replaceInjectedVars(request, injectedVars) { @@ -222,10 +222,7 @@ export function uiRenderMixin(kbnServer, server, config) { ...kbnServer.newPlatform.setup.core.plugins.uiPlugins.public.entries() ].map(([id, plugin]) => ({ id, plugin })); - const nonce = await generateCSPNonce(); - const response = h.view('ui_app', { - nonce, strictCsp: config.get('csp.strict'), uiPublicUrl: `${basePath}/ui`, bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`, @@ -261,7 +258,7 @@ export function uiRenderMixin(kbnServer, server, config) { }, }); - const csp = createCSPRuleString(config.get('csp.rules'), nonce); + const csp = createCSPRuleString(config.get('csp.rules')); response.header('content-security-policy', csp); return response; diff --git a/src/legacy/ui/ui_render/views/chrome.pug b/src/legacy/ui/ui_render/views/chrome.pug index ffadf0964aee..9cb99f961767 100644 --- a/src/legacy/ui/ui_render/views/chrome.pug +++ b/src/legacy/ui/ui_render/views/chrome.pug @@ -300,6 +300,6 @@ html(lang=locale) block head body - kbn-csp(data=JSON.stringify({ nonce, strictCsp })) + kbn-csp(data=JSON.stringify({ strictCsp })) kbn-injected-metadata(data=JSON.stringify(injectedMetadata)) block content diff --git a/src/legacy/ui/ui_render/views/ui_app.pug b/src/legacy/ui/ui_render/views/ui_app.pug index feeaff027993..5bbcc51e7745 100644 --- a/src/legacy/ui/ui_render/views/ui_app.pug +++ b/src/legacy/ui/ui_render/views/ui_app.pug @@ -132,9 +132,9 @@ block content | #{i18n('common.ui.legacyBrowserMessage', { defaultMessage: 'This Kibana installation has strict security requirements enabled that your current browser does not meet.' })} script. - // Since this script tag does not contain a nonce, this code will not run + // Since this is an unsafe inline script, 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(src=bootstrapScriptUrl, nonce=nonce) + script(src=bootstrapScriptUrl) diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index bfeb12f0eba7..99e57f007eff 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -408,10 +408,7 @@ export default class BaseOptimizer { 'node_modules', fromRoot('node_modules'), ], - alias: { - ...this.uiBundles.getAliases(), - 'dll/set_csp_nonce$': require.resolve('./dynamic_dll_plugin/public/set_csp_nonce') - } + alias: this.uiBundles.getAliases(), }, performance: { diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js index 342ac96425a6..46aa472b5ae5 100644 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js @@ -57,10 +57,7 @@ function generateDLL(config) { resolve: { extensions: ['.js', '.json'], mainFields: ['browser', 'browserify', 'main'], - alias: { - ...dllAlias, - 'dll/set_csp_nonce$': require.resolve('./public/set_csp_nonce') - }, + alias: dllAlias, modules: [ 'webpackShims', fromRoot('webpackShims'), diff --git a/src/optimize/dynamic_dll_plugin/dll_entry_template.js b/src/optimize/dynamic_dll_plugin/dll_entry_template.js index e1873b61662b..584bf0c9e3d3 100644 --- a/src/optimize/dynamic_dll_plugin/dll_entry_template.js +++ b/src/optimize/dynamic_dll_plugin/dll_entry_template.js @@ -18,10 +18,8 @@ */ export function dllEntryTemplate(requirePaths = []) { - return [ - `require('dll/set_csp_nonce');`, - ...requirePaths - .map(path => `require('${path}');`) - .sort() - ].join('\n'); + return requirePaths + .map(path => `require('${path}');`) + .sort() + .join('\n'); } diff --git a/src/optimize/dynamic_dll_plugin/public/set_csp_nonce.js b/src/optimize/dynamic_dll_plugin/public/set_csp_nonce.js deleted file mode 100644 index a5ad91443d2d..000000000000 --- a/src/optimize/dynamic_dll_plugin/public/set_csp_nonce.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// eslint-disable-next-line camelcase, no-undef -__webpack_nonce__ = window.__kbnNonce__; diff --git a/test/api_integration/apis/general/csp.js b/test/api_integration/apis/general/csp.js index 8c28d7e6d7dd..ae5cf27ff68f 100644 --- a/test/api_integration/apis/general/csp.js +++ b/test/api_integration/apis/general/csp.js @@ -34,16 +34,9 @@ export default function ({ getService }) { return [key, parts]; })); - // ensure script-src uses a nonce, and remove it so we can .eql everything else - const scriptSrc = parsed.get('script-src'); - expect(scriptSrc).to.be.an(Array); - const nonceIndex = scriptSrc.findIndex(value => value.startsWith(`'nonce-`)); - expect(nonceIndex).greaterThan(-1); - scriptSrc.splice(nonceIndex, 1); - const entries = Array.from(parsed.entries()); expect(entries).to.eql([ - [ 'script-src', [ '\'unsafe-eval\'' ] ], + [ 'script-src', [ '\'unsafe-eval\'', '\'self\'' ] ], [ 'worker-src', [ 'blob:' ] ], [ 'child-src', [ 'blob:' ] ], [ 'style-src', [ '\'unsafe-inline\'', '\'self\'' ] ] diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js index f546b53d9961..5e2bfce7ada1 100644 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js @@ -9,7 +9,7 @@ import Joi from 'joi'; import { schema } from '@kbn/config-schema'; import { canRedirectRequest, wrapError, OIDCAuthenticationFlow } from '../../../../../../../plugins/security/server'; import { KibanaRequest } from '../../../../../../../../src/core/server'; -import { createCSPRuleString, generateCSPNonce } from '../../../../../../../../src/legacy/server/csp'; +import { createCSPRuleString } from '../../../../../../../../src/legacy/server/csp'; export function initAuthenticateApi({ authc: { login, logout }, config }, server) { @@ -96,12 +96,11 @@ export function initAuthenticateApi({ authc: { login, logout }, config }, server const legacyConfig = server.config(); const basePath = legacyConfig.get('server.basePath'); - const nonce = await generateCSPNonce(); - const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce); + const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules')); return h.response(` Kibana OpenID Connect Login -