From 6ff4a80d69d1b5a1889e64b35b328ae4e6ac4c9f Mon Sep 17 00:00:00 2001 From: Eli Perelman Date: Fri, 13 Dec 2019 15:57:17 -0600 Subject: [PATCH] Move CSP options to new platform (#52698) * Move CSP options to new platform * Expose SharedGlobalConfig from root * Derive CSP options from config * Consolidate CSP configuration with HTTP config * Fix outstanding config renames * Remove legacy CSP configuration calls, migrate to platform properties * Revise docs * Fix test from type change * Expose ICspConfig, consolidate and simplify CSP defaults access * Rebase and update docs * Remove legacy API from route definition params, review nits * Clean up config path usages for consistency * Regenerate docs --- .../kibana-plugin-server.cspconfig.default.md | 11 ++ .../kibana-plugin-server.cspconfig.header.md | 11 ++ .../server/kibana-plugin-server.cspconfig.md | 28 +++++ .../kibana-plugin-server.cspconfig.rules.md | 11 ++ .../kibana-plugin-server.cspconfig.strict.md | 11 ++ ...gin-server.cspconfig.warnlegacybrowsers.md | 11 ++ ...bana-plugin-server.httpservicesetup.csp.md | 13 ++ .../kibana-plugin-server.httpservicesetup.md | 1 + .../kibana-plugin-server.icspconfig.header.md | 13 ++ .../server/kibana-plugin-server.icspconfig.md | 23 ++++ .../kibana-plugin-server.icspconfig.rules.md | 13 ++ .../kibana-plugin-server.icspconfig.strict.md | 13 ++ ...in-server.icspconfig.warnlegacybrowsers.md | 13 ++ .../core/server/kibana-plugin-server.md | 2 + src/core/server/csp/config.ts | 42 +++++++ src/core/server/csp/csp_config.test.ts | 97 +++++++++++++++ src/core/server/csp/csp_config.ts | 77 ++++++++++++ src/{legacy => core}/server/csp/index.ts | 15 +-- src/core/server/http/http_config.test.ts | 3 + src/core/server/http/http_config.ts | 29 +++-- src/core/server/http/http_server.ts | 2 + src/core/server/http/http_service.mock.ts | 2 + src/core/server/http/http_service.test.ts | 2 + src/core/server/http/http_service.ts | 27 +++-- src/core/server/http/http_tools.test.ts | 26 ++-- src/core/server/http/types.ts | 6 + src/core/server/index.ts | 1 + .../legacy_object_to_config_adapter.test.ts | 16 +++ src/core/server/legacy/legacy_service.ts | 29 +++-- src/core/server/mocks.ts | 2 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 25 ++++ src/core/server/server.ts | 2 + src/core/server/types.ts | 1 + .../csp_usage_collector/csp_collector.test.ts | 113 +++++++++--------- .../lib/csp_usage_collector/csp_collector.ts | 19 ++- src/legacy/server/config/schema.js | 9 +- src/legacy/server/csp/index.test.ts | 61 ---------- src/legacy/ui/ui_render/ui_render_mixin.js | 9 +- x-pack/legacy/plugins/security/index.js | 2 - x-pack/plugins/security/server/plugin.ts | 3 +- .../routes/authentication/basic.test.ts | 3 +- .../routes/authentication/common.test.ts | 3 +- .../routes/authentication/index.test.ts | 2 +- .../server/routes/authentication/index.ts | 4 +- .../server/routes/authentication/oidc.ts | 12 +- .../server/routes/authentication/saml.test.ts | 3 +- .../server/routes/authentication/saml.ts | 12 +- .../security/server/routes/index.mock.ts | 2 +- .../plugins/security/server/routes/index.ts | 3 +- .../routes/users/change_password.test.ts | 3 +- 51 files changed, 600 insertions(+), 242 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.cspconfig.default.md create mode 100644 docs/development/core/server/kibana-plugin-server.cspconfig.header.md create mode 100644 docs/development/core/server/kibana-plugin-server.cspconfig.md create mode 100644 docs/development/core/server/kibana-plugin-server.cspconfig.rules.md create mode 100644 docs/development/core/server/kibana-plugin-server.cspconfig.strict.md create mode 100644 docs/development/core/server/kibana-plugin-server.cspconfig.warnlegacybrowsers.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpservicesetup.csp.md create mode 100644 docs/development/core/server/kibana-plugin-server.icspconfig.header.md create mode 100644 docs/development/core/server/kibana-plugin-server.icspconfig.md create mode 100644 docs/development/core/server/kibana-plugin-server.icspconfig.rules.md create mode 100644 docs/development/core/server/kibana-plugin-server.icspconfig.strict.md create mode 100644 docs/development/core/server/kibana-plugin-server.icspconfig.warnlegacybrowsers.md create mode 100644 src/core/server/csp/config.ts create mode 100644 src/core/server/csp/csp_config.test.ts create mode 100644 src/core/server/csp/csp_config.ts rename src/{legacy => core}/server/csp/index.ts (70%) delete mode 100644 src/legacy/server/csp/index.test.ts diff --git a/docs/development/core/server/kibana-plugin-server.cspconfig.default.md b/docs/development/core/server/kibana-plugin-server.cspconfig.default.md new file mode 100644 index 0000000000000..56e6cf35cdd13 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.cspconfig.default.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CspConfig](./kibana-plugin-server.cspconfig.md) > [DEFAULT](./kibana-plugin-server.cspconfig.default.md) + +## CspConfig.DEFAULT property + +Signature: + +```typescript +static readonly DEFAULT: CspConfig; +``` diff --git a/docs/development/core/server/kibana-plugin-server.cspconfig.header.md b/docs/development/core/server/kibana-plugin-server.cspconfig.header.md new file mode 100644 index 0000000000000..e3a3d5d712a42 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.cspconfig.header.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CspConfig](./kibana-plugin-server.cspconfig.md) > [header](./kibana-plugin-server.cspconfig.header.md) + +## CspConfig.header property + +Signature: + +```typescript +readonly header: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.cspconfig.md b/docs/development/core/server/kibana-plugin-server.cspconfig.md new file mode 100644 index 0000000000000..e5276991be404 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.cspconfig.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CspConfig](./kibana-plugin-server.cspconfig.md) + +## CspConfig class + +CSP configuration for use in Kibana. + +Signature: + +```typescript +export declare class CspConfig implements ICspConfig +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [DEFAULT](./kibana-plugin-server.cspconfig.default.md) | static | CspConfig | | +| [header](./kibana-plugin-server.cspconfig.header.md) | | string | | +| [rules](./kibana-plugin-server.cspconfig.rules.md) | | string[] | | +| [strict](./kibana-plugin-server.cspconfig.strict.md) | | boolean | | +| [warnLegacyBrowsers](./kibana-plugin-server.cspconfig.warnlegacybrowsers.md) | | boolean | | + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `CspConfig` class. + diff --git a/docs/development/core/server/kibana-plugin-server.cspconfig.rules.md b/docs/development/core/server/kibana-plugin-server.cspconfig.rules.md new file mode 100644 index 0000000000000..c5270c2375dc1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.cspconfig.rules.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CspConfig](./kibana-plugin-server.cspconfig.md) > [rules](./kibana-plugin-server.cspconfig.rules.md) + +## CspConfig.rules property + +Signature: + +```typescript +readonly rules: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.cspconfig.strict.md b/docs/development/core/server/kibana-plugin-server.cspconfig.strict.md new file mode 100644 index 0000000000000..3ac48edd374c9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.cspconfig.strict.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CspConfig](./kibana-plugin-server.cspconfig.md) > [strict](./kibana-plugin-server.cspconfig.strict.md) + +## CspConfig.strict property + +Signature: + +```typescript +readonly strict: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.cspconfig.warnlegacybrowsers.md b/docs/development/core/server/kibana-plugin-server.cspconfig.warnlegacybrowsers.md new file mode 100644 index 0000000000000..59d661593d940 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.cspconfig.warnlegacybrowsers.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CspConfig](./kibana-plugin-server.cspconfig.md) > [warnLegacyBrowsers](./kibana-plugin-server.cspconfig.warnlegacybrowsers.md) + +## CspConfig.warnLegacyBrowsers property + +Signature: + +```typescript +readonly warnLegacyBrowsers: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.csp.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.csp.md new file mode 100644 index 0000000000000..7bf83305613ea --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.csp.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [csp](./kibana-plugin-server.httpservicesetup.csp.md) + +## HttpServiceSetup.csp property + +The CSP config used for Kibana. + +Signature: + +```typescript +csp: ICspConfig; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 25eebf1c06d01..99d4caf40c0d3 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -19,6 +19,7 @@ export interface HttpServiceSetup | [basePath](./kibana-plugin-server.httpservicesetup.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-server.ibasepath.md). | | [createCookieSessionStorageFactory](./kibana-plugin-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | | [createRouter](./kibana-plugin-server.httpservicesetup.createrouter.md) | () => IRouter | Provides ability to declare a handler function for a particular path and HTTP request method. | +| [csp](./kibana-plugin-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. | | [isTlsEnabled](./kibana-plugin-server.httpservicesetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | diff --git a/docs/development/core/server/kibana-plugin-server.icspconfig.header.md b/docs/development/core/server/kibana-plugin-server.icspconfig.header.md new file mode 100644 index 0000000000000..d757863fdc12d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.icspconfig.header.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICspConfig](./kibana-plugin-server.icspconfig.md) > [header](./kibana-plugin-server.icspconfig.header.md) + +## ICspConfig.header property + +The CSP rules in a formatted directives string for use in a `Content-Security-Policy` header. + +Signature: + +```typescript +readonly header: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.icspconfig.md b/docs/development/core/server/kibana-plugin-server.icspconfig.md new file mode 100644 index 0000000000000..fb8188386a376 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.icspconfig.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICspConfig](./kibana-plugin-server.icspconfig.md) + +## ICspConfig interface + +CSP configuration for use in Kibana. + +Signature: + +```typescript +export interface ICspConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [header](./kibana-plugin-server.icspconfig.header.md) | string | The CSP rules in a formatted directives string for use in a Content-Security-Policy header. | +| [rules](./kibana-plugin-server.icspconfig.rules.md) | string[] | The CSP rules used for Kibana. | +| [strict](./kibana-plugin-server.icspconfig.strict.md) | boolean | Specify whether browsers that do not support CSP should be able to use Kibana. Use true to block and false to allow. | +| [warnLegacyBrowsers](./kibana-plugin-server.icspconfig.warnlegacybrowsers.md) | boolean | Specify whether users with legacy browsers should be warned about their lack of Kibana security compliance. | + diff --git a/docs/development/core/server/kibana-plugin-server.icspconfig.rules.md b/docs/development/core/server/kibana-plugin-server.icspconfig.rules.md new file mode 100644 index 0000000000000..6216e6d817136 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.icspconfig.rules.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICspConfig](./kibana-plugin-server.icspconfig.md) > [rules](./kibana-plugin-server.icspconfig.rules.md) + +## ICspConfig.rules property + +The CSP rules used for Kibana. + +Signature: + +```typescript +readonly rules: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.icspconfig.strict.md b/docs/development/core/server/kibana-plugin-server.icspconfig.strict.md new file mode 100644 index 0000000000000..4ab97ad9f665a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.icspconfig.strict.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICspConfig](./kibana-plugin-server.icspconfig.md) > [strict](./kibana-plugin-server.icspconfig.strict.md) + +## ICspConfig.strict property + +Specify whether browsers that do not support CSP should be able to use Kibana. Use `true` to block and `false` to allow. + +Signature: + +```typescript +readonly strict: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.icspconfig.warnlegacybrowsers.md b/docs/development/core/server/kibana-plugin-server.icspconfig.warnlegacybrowsers.md new file mode 100644 index 0000000000000..aea35f0569448 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.icspconfig.warnlegacybrowsers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICspConfig](./kibana-plugin-server.icspconfig.md) > [warnLegacyBrowsers](./kibana-plugin-server.icspconfig.warnlegacybrowsers.md) + +## ICspConfig.warnLegacyBrowsers property + +Specify whether users with legacy browsers should be warned about their lack of Kibana security compliance. + +Signature: + +```typescript +readonly warnLegacyBrowsers: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 06dcede0f2dfe..e97ecbcfaf739 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -18,6 +18,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path | | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | +| [CspConfig](./kibana-plugin-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) | | @@ -64,6 +65,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | +| [ICspConfig](./kibana-plugin-server.icspconfig.md) | CSP configuration for use in Kibana. | | [IKibanaResponse](./kibana-plugin-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [IndexSettingsDeprecationInfo](./kibana-plugin-server.indexsettingsdeprecationinfo.md) | | diff --git a/src/core/server/csp/config.ts b/src/core/server/csp/config.ts new file mode 100644 index 0000000000000..9e2c94bb2d2d9 --- /dev/null +++ b/src/core/server/csp/config.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; + +/** + * @internal + */ +export type CspConfigType = TypeOf; + +export const config = { + // TODO: Move this to server.csp using config deprecations + // ? https://github.com/elastic/kibana/pull/52251 + path: 'csp', + schema: schema.object({ + rules: schema.arrayOf(schema.string(), { + defaultValue: [ + `script-src 'unsafe-eval' 'self'`, + `worker-src blob: 'self'`, + `style-src 'unsafe-inline' 'self'`, + ], + }), + strict: schema.boolean({ defaultValue: false }), + warnLegacyBrowsers: schema.boolean({ defaultValue: true }), + }), +}; diff --git a/src/core/server/csp/csp_config.test.ts b/src/core/server/csp/csp_config.test.ts new file mode 100644 index 0000000000000..45fa8445791b0 --- /dev/null +++ b/src/core/server/csp/csp_config.test.ts @@ -0,0 +1,97 @@ +/* + * 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. + */ + +import { CspConfig } from '.'; + +// CSP rules aren't strictly additive, so any change can potentially expand or +// restrict the policy in a way we consider a breaking change. For that reason, +// we test the default rules exactly so any change to those rules gets flagged +// for manual review. In other words, this test is intentionally fragile to draw +// extra attention if defaults are modified in any way. +// +// A test failure here does not necessarily mean this change cannot be made, +// but any change here should undergo sufficient scrutiny by the Kibana +// security team. +// +// The tests use inline snapshots to make it as easy as possible to identify +// the nature of a change in defaults during a PR review. + +describe('CspConfig', () => { + test('DEFAULT', () => { + expect(CspConfig.DEFAULT).toMatchInlineSnapshot(` + CspConfig { + "header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + "rules": Array [ + "script-src 'unsafe-eval' 'self'", + "worker-src blob: 'self'", + "style-src 'unsafe-inline' 'self'", + ], + "strict": true, + "warnLegacyBrowsers": true, + } + `); + }); + + test('defaults from config', () => { + expect(new CspConfig()).toMatchInlineSnapshot(` + CspConfig { + "header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + "rules": Array [ + "script-src 'unsafe-eval' 'self'", + "worker-src blob: 'self'", + "style-src 'unsafe-inline' 'self'", + ], + "strict": true, + "warnLegacyBrowsers": true, + } + `); + }); + + test('creates from partial config', () => { + expect(new CspConfig({ strict: false, warnLegacyBrowsers: false })).toMatchInlineSnapshot(` + CspConfig { + "header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + "rules": Array [ + "script-src 'unsafe-eval' 'self'", + "worker-src blob: 'self'", + "style-src 'unsafe-inline' 'self'", + ], + "strict": false, + "warnLegacyBrowsers": false, + } + `); + }); + + test('computes header from rules', () => { + const cspConfig = new CspConfig({ rules: ['alpha', 'beta', 'gamma'] }); + + expect(cspConfig).toMatchInlineSnapshot(` + CspConfig { + "header": "alpha; beta; gamma", + "rules": Array [ + "alpha", + "beta", + "gamma", + ], + "strict": true, + "warnLegacyBrowsers": true, + } + `); + }); +}); diff --git a/src/core/server/csp/csp_config.ts b/src/core/server/csp/csp_config.ts new file mode 100644 index 0000000000000..bb57702a4a241 --- /dev/null +++ b/src/core/server/csp/csp_config.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +import { config } from './config'; + +const DEFAULT_CONFIG = Object.freeze(config.schema.validate({})); + +/** + * CSP configuration for use in Kibana. + * @public + */ +export interface ICspConfig { + /** + * The CSP rules used for Kibana. + */ + readonly rules: string[]; + + /** + * Specify whether browsers that do not support CSP should be + * able to use Kibana. Use `true` to block and `false` to allow. + */ + readonly strict: boolean; + + /** + * Specify whether users with legacy browsers should be warned + * about their lack of Kibana security compliance. + */ + readonly warnLegacyBrowsers: boolean; + + /** + * The CSP rules in a formatted directives string for use + * in a `Content-Security-Policy` header. + */ + readonly header: string; +} + +/** + * CSP configuration for use in Kibana. + * @public + */ +export class CspConfig implements ICspConfig { + static readonly DEFAULT = new CspConfig(); + + public readonly rules: string[]; + public readonly strict: boolean; + public readonly warnLegacyBrowsers: boolean; + public readonly header: string; + + /** + * Returns the default CSP configuration when passed with no config + * @internal + */ + constructor(rawCspConfig: Partial> = {}) { + const source = { ...DEFAULT_CONFIG, ...rawCspConfig }; + + this.rules = source.rules; + this.strict = source.strict; + this.warnLegacyBrowsers = source.warnLegacyBrowsers; + this.header = source.rules.join('; '); + } +} diff --git a/src/legacy/server/csp/index.ts b/src/core/server/csp/index.ts similarity index 70% rename from src/legacy/server/csp/index.ts rename to src/core/server/csp/index.ts index ee07762dfcdb0..a9e320ac5afa5 100644 --- a/src/legacy/server/csp/index.ts +++ b/src/core/server/csp/index.ts @@ -17,16 +17,7 @@ * under the License. */ -export const DEFAULT_CSP_RULES = Object.freeze([ - `script-src 'unsafe-eval' 'self'`, - `worker-src blob: 'self'`, - `style-src 'unsafe-inline' 'self'`, -]); +import { CspConfig, ICspConfig } from './csp_config'; +import { CspConfigType, config } from './config'; -export const DEFAULT_CSP_STRICT = false; - -export const DEFAULT_CSP_WARN_LEGACY_BROWSERS = true; - -export function createCSPRuleString(rules: string[]) { - return rules.join('; '); -} +export { CspConfig, CspConfigType, config, ICspConfig }; diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 1ee7e13d5e851..888313e1478cb 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -256,6 +256,7 @@ describe('with TLS', () => { clientAuthentication: 'none', }, }), + {} as any, Env.createDefault(getEnvOptions()) ); @@ -273,6 +274,7 @@ describe('with TLS', () => { clientAuthentication: 'optional', }, }), + {} as any, Env.createDefault(getEnvOptions()) ); @@ -290,6 +292,7 @@ describe('with TLS', () => { clientAuthentication: 'required', }, }), + {} as any, Env.createDefault(getEnvOptions()) ); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index cb7726de4da5a..912459c83df6e 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -19,6 +19,7 @@ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import { Env } from '../config'; +import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /(^$|^\/.*[^\/]$)/; @@ -132,23 +133,25 @@ export class HttpConfig { public defaultRoute?: string; public ssl: SslConfig; public compression: { enabled: boolean; referrerWhitelist?: string[] }; + public csp: ICspConfig; /** * @internal */ - constructor(rawConfig: HttpConfigType, env: Env) { - this.autoListen = rawConfig.autoListen; - this.host = rawConfig.host; - this.port = rawConfig.port; - this.cors = rawConfig.cors; - this.maxPayload = rawConfig.maxPayload; - this.basePath = rawConfig.basePath; - this.keepaliveTimeout = rawConfig.keepaliveTimeout; - this.socketTimeout = rawConfig.socketTimeout; - this.rewriteBasePath = rawConfig.rewriteBasePath; + constructor(rawHttpConfig: HttpConfigType, rawCspConfig: CspConfigType, env: Env) { + this.autoListen = rawHttpConfig.autoListen; + this.host = rawHttpConfig.host; + this.port = rawHttpConfig.port; + this.cors = rawHttpConfig.cors; + this.maxPayload = rawHttpConfig.maxPayload; + this.basePath = rawHttpConfig.basePath; + this.keepaliveTimeout = rawHttpConfig.keepaliveTimeout; + this.socketTimeout = rawHttpConfig.socketTimeout; + this.rewriteBasePath = rawHttpConfig.rewriteBasePath; this.publicDir = env.staticFilesDir; - this.ssl = new SslConfig(rawConfig.ssl || {}); - this.defaultRoute = rawConfig.defaultRoute; - this.compression = rawConfig.compression; + this.ssl = new SslConfig(rawHttpConfig.ssl || {}); + this.defaultRoute = rawHttpConfig.defaultRoute; + this.compression = rawHttpConfig.compression; + this.csp = new CspConfig(rawCspConfig); } } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 244b3cca60f31..994a6cced8914 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -46,6 +46,7 @@ export interface HttpServerSetup { */ registerRouter: (router: IRouter) => void; basePath: HttpServiceSetup['basePath']; + csp: HttpServiceSetup['csp']; createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory']; registerAuth: HttpServiceSetup['registerAuth']; registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; @@ -109,6 +110,7 @@ export class HttpServer { this.createCookieSessionStorageFactory(cookieOptions, config.basePath), registerAuth: this.registerAuth.bind(this), basePath: basePathService, + csp: config.csp, auth: { get: this.authState.get, isAuthenticated: this.authState.isAuthenticated, diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 444aa04171dbd..1668b409050b7 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -18,6 +18,7 @@ */ import { Server } from 'hapi'; +import { CspConfig } from '../csp'; import { mockRouter } from './router/router.mock'; import { InternalHttpServiceSetup } from './types'; import { HttpService } from './http_service'; @@ -55,6 +56,7 @@ const createSetupContractMock = () => { registerOnPreResponse: jest.fn(), createRouter: jest.fn().mockImplementation(() => mockRouter.create({})), basePath: createBasePathMock(), + csp: CspConfig.DEFAULT, auth: { get: jest.fn(), isAuthenticated: jest.fn(), diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index a2546709a318c..8b500caf217dc 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -28,6 +28,7 @@ import { ConfigService, Env } from '../config'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; +import { config as cspConfig } from '../csp'; const logger = loggingServiceMock.create(); const env = Env.createDefault(getEnvOptions()); @@ -45,6 +46,7 @@ const createConfigService = (value: Partial = {}) => { logger ); configService.setSchema(config.path, config.schema); + configService.setSchema(cspConfig.path, cspConfig.schema); return configService; }; const contextSetup = contextServiceMock.createSetupContract(); diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index caebd768c70e5..faeae0b559b6b 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, combineLatest } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { Server } from 'hapi'; @@ -28,9 +28,10 @@ import { Logger } from '../logging'; import { ContextSetup } from '../context'; import { CoreContext } from '../core_context'; import { PluginOpaqueId } from '../plugins'; +import { CspConfigType, config as cspConfig } from '../csp'; import { Router } from './router'; -import { HttpConfig, HttpConfigType } from './http_config'; +import { HttpConfig, HttpConfigType, config as httpConfig } from './http_config'; import { HttpServer } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; @@ -60,16 +61,16 @@ export class HttpService implements CoreService('server') - .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); - - this.httpServer = new HttpServer(coreContext.logger, 'Kibana'); - this.httpsRedirectServer = new HttpsRedirectServer( - coreContext.logger.get('http', 'redirect', 'server') - ); + const { logger, configService, env } = coreContext; + + this.logger = logger; + this.log = logger.get('http'); + this.config$ = combineLatest( + configService.atPath(httpConfig.path), + configService.atPath(cspConfig.path) + ).pipe(map(([http, csp]) => new HttpConfig(http, csp, env))); + this.httpServer = new HttpServer(logger, 'Kibana'); + this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } public async setup(deps: SetupDeps) { @@ -79,7 +80,7 @@ export class HttpService implements CoreService { certificate: 'some-certificate-path', }, }), + {} as any, Env.createDefault(getEnvOptions()) ); expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(` - Object { - "ca": undefined, - "cert": "content-some-certificate-path", - "ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", - "honorCipherOrder": true, - "key": "content-some-key-path", - "passphrase": undefined, - "rejectUnauthorized": false, - "requestCert": false, - "secureOptions": 67108864, - } - `); + Object { + "ca": undefined, + "cert": "content-some-certificate-path", + "ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", + "honorCipherOrder": true, + "key": "content-some-key-path", + "passphrase": undefined, + "rejectUnauthorized": false, + "requestCert": false, + "secureOptions": 67108864, + } + `); }); it('properly configures TLS with client authentication', () => { @@ -151,6 +152,7 @@ describe('getServerOptions', () => { clientAuthentication: 'required', }, }), + {} as any, Env.createDefault(getEnvOptions()) ); diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 94c1982a18c0a..92217515a22a1 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -17,6 +17,7 @@ * under the License. */ import { IContextProvider, IContextContainer } from '../context'; +import { ICspConfig } from '../csp'; import { RequestHandler, IRouter } from './router'; import { HttpServerSetup } from './http_server'; import { SessionStorageCookieOptions } from './cookie_session_storage'; @@ -182,6 +183,11 @@ export interface HttpServiceSetup { */ basePath: IBasePath; + /** + * The CSP config used for Kibana. + */ + csp: ICspConfig; + /** * Flag showing whether a server was configured to use TLS connection. */ diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c304958f78bb7..835c5872d51a3 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -68,6 +68,7 @@ export { HandlerParameters, } from './context'; export { CoreId } from './core_context'; +export { CspConfig, ICspConfig } from './csp'; export { ClusterClient, IClusterClient, diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts index 201f761701a35..db2bc117280ca 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts @@ -45,6 +45,22 @@ describe('#get', () => { expect(configAdapter.get('container')).toEqual({ value: 'some' }); }); + test('correctly handles csp config.', () => { + const configAdapter = new LegacyObjectToConfigAdapter({ + csp: { + rules: ['strict'], + }, + }); + + expect(configAdapter.get('csp')).toMatchInlineSnapshot(` + Object { + "rules": Array [ + "strict", + ], + } + `); + }); + test('correctly handles silent logging config.', () => { const configAdapter = new LegacyObjectToConfigAdapter({ logging: { silent: true }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 4c2e57dc69b29..662cc0bdf2f3a 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -25,8 +25,9 @@ import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { SavedObjectsLegacyUiExports } from '../types'; import { Config, ConfigDeprecationProvider } from '../config'; import { CoreContext } from '../core_context'; -import { DevConfig, DevConfigType } from '../dev'; -import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; +import { CspConfigType, config as cspConfig } from '../csp'; +import { DevConfig, DevConfigType, config as devConfig } from '../dev'; +import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; import { findLegacyPluginSpecs } from './plugins'; @@ -112,13 +113,16 @@ export class LegacyService implements CoreService { private settings: Record | undefined; constructor(private readonly coreContext: CoreContext) { - this.log = coreContext.logger.get('legacy-service'); - this.devConfig$ = coreContext.configService - .atPath('dev') + const { logger, configService, env } = coreContext; + + this.log = logger.get('legacy-service'); + this.devConfig$ = configService + .atPath(devConfig.path) .pipe(map(rawConfig => new DevConfig(rawConfig))); - this.httpConfig$ = coreContext.configService - .atPath('server') - .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); + this.httpConfig$ = combineLatest( + configService.atPath(httpConfig.path), + configService.atPath(cspConfig.path) + ).pipe(map(([http, csp]) => new HttpConfig(http, csp, env))); } public async discoverPlugins(): Promise { @@ -240,8 +244,8 @@ export class LegacyService implements CoreService { ? combineLatest(this.devConfig$, this.httpConfig$).pipe( first(), map( - ([devConfig, httpConfig]) => - new BasePathProxyServer(this.coreContext.logger.get('server'), httpConfig, devConfig) + ([dev, http]) => + new BasePathProxyServer(this.coreContext.logger.get('server'), http, dev) ) ) : EMPTY; @@ -284,6 +288,7 @@ export class LegacyService implements CoreService { registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, registerOnPreResponse: setupDeps.core.http.registerOnPreResponse, basePath: setupDeps.core.http.basePath, + csp: setupDeps.core.http.csp, isTlsEnabled: setupDeps.core.http.isTlsEnabled, }, savedObjects: { @@ -339,9 +344,9 @@ export class LegacyService implements CoreService { require('../../../cli/repl').startRepl(kbnServer); } - const httpConfig = await this.httpConfig$.pipe(first()).toPromise(); + const { autoListen } = await this.httpConfig$.pipe(first()).toPromise(); - if (httpConfig.autoListen) { + if (autoListen) { try { await kbnServer.listen(); } catch (err) { diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index c07caaa04ba52..07b60e771d643 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -19,6 +19,7 @@ import { of } from 'rxjs'; import { duration } from 'moment'; import { PluginInitializerContext, CoreSetup, CoreStart } from '.'; +import { CspConfig } from './csp'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; @@ -92,6 +93,7 @@ function createCoreSetupMock() { registerOnPostAuth: httpService.registerOnPostAuth, registerOnPreResponse: httpService.registerOnPreResponse, basePath: httpService.basePath, + csp: CspConfig.DEFAULT, isTlsEnabled: httpService.isTlsEnabled, createRouter: jest.fn(), registerRouteHandlerContext: jest.fn(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 6829784e6e0a1..26c65baf95535 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -161,6 +161,7 @@ export function createPluginSetupContext( registerOnPostAuth: deps.http.registerOnPostAuth, registerOnPreResponse: deps.http.registerOnPreResponse, basePath: deps.http.basePath, + csp: deps.http.csp, isTlsEnabled: deps.http.isTlsEnabled, }, savedObjects: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 13ca4b582c98e..780a1532a859e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -574,6 +574,22 @@ export interface CoreStart { savedObjects: SavedObjectsServiceStart; } +// @public +export class CspConfig implements ICspConfig { + // @internal + constructor(rawCspConfig?: Partial>); + // (undocumented) + static readonly DEFAULT: CspConfig; + // (undocumented) + readonly header: string; + // (undocumented) + readonly rules: string[]; + // (undocumented) + readonly strict: boolean; + // (undocumented) + readonly warnLegacyBrowsers: boolean; +} + // @public export interface CustomHttpResponseOptions { body?: T; @@ -713,6 +729,7 @@ export interface HttpServiceSetup { basePath: IBasePath; createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; createRouter: () => IRouter; + csp: ICspConfig; isTlsEnabled: boolean; registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; @@ -741,6 +758,14 @@ export interface IContextContainer> { // @public export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; +// @public +export interface ICspConfig { + readonly header: string; + readonly rules: string[]; + readonly strict: boolean; + readonly warnLegacyBrowsers: boolean; +} + // @public export interface IKibanaResponse { // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e7bc57ea5fb94..725a45f131992 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -35,6 +35,7 @@ import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; +import { config as cspConfig } from './csp'; import { config as elasticsearchConfig } from './elasticsearch'; import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; @@ -218,6 +219,7 @@ export class Server { public async setupCoreConfig() { const schemas: Array<[ConfigPath, Type]> = [ [pathConfig.path, pathConfig.schema], + [cspConfig.path, cspConfig.schema], [elasticsearchConfig.path, elasticsearchConfig.schema], [loggingConfig.path, loggingConfig.schema], [httpConfig.path, httpConfig.schema], diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 4878fb9ccae19..6e3e6bfe208a6 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -22,3 +22,4 @@ export { PluginOpaqueId } from './plugins/types'; export * from './saved_objects/types'; export * from './ui_settings/types'; export { EnvironmentMode, PackageInfo } from './config/types'; +export { ICspConfig } from './csp'; diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts index 36e7dc81d4708..395cb60587832 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts @@ -17,79 +17,74 @@ * under the License. */ -import sinon from 'sinon'; -import { Server } from 'hapi'; -import { DEFAULT_CSP_RULES } from '../../../../../server/csp'; +import { CspConfig, ICspConfig } from '../../../../../../core/server'; import { createCspCollector } from './csp_collector'; -interface MockConfig { - get: (x: string) => any; -} - -const getMockKbnServer = (mockConfig: MockConfig) => ({ - config: () => mockConfig, +const createMockKbnServer = () => ({ + newPlatform: { + setup: { + core: { + http: { + csp: new CspConfig(), + }, + }, + }, + }, }); -test('fetches whether strict mode is enabled', async () => { - const { collector, mockConfig } = setupCollector(); +describe('csp collector', () => { + let kbnServer: ReturnType; - expect((await collector.fetch()).strict).toEqual(true); + function updateCsp(config: Partial) { + kbnServer.newPlatform.setup.core.http.csp = new CspConfig(config); + } - mockConfig.get.withArgs('csp.strict').returns(false); - expect((await collector.fetch()).strict).toEqual(false); -}); + beforeEach(() => { + kbnServer = createMockKbnServer(); + }); -test('fetches whether the legacy browser warning is enabled', async () => { - const { collector, mockConfig } = setupCollector(); + test('fetches whether strict mode is enabled', async () => { + const collector = createCspCollector(kbnServer as any); - expect((await collector.fetch()).warnLegacyBrowsers).toEqual(true); + expect((await collector.fetch()).strict).toEqual(true); - mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(false); - expect((await collector.fetch()).warnLegacyBrowsers).toEqual(false); -}); + updateCsp({ strict: false }); + expect((await collector.fetch()).strict).toEqual(false); + }); -test('fetches whether the csp rules have been changed or not', async () => { - const { collector, mockConfig } = setupCollector(); + test('fetches whether the legacy browser warning is enabled', async () => { + const collector = createCspCollector(kbnServer as any); - expect((await collector.fetch()).rulesChangedFromDefault).toEqual(false); + expect((await collector.fetch()).warnLegacyBrowsers).toEqual(true); - mockConfig.get.withArgs('csp.rules').returns(['not', 'default']); - expect((await collector.fetch()).rulesChangedFromDefault).toEqual(true); -}); + updateCsp({ warnLegacyBrowsers: false }); + expect((await collector.fetch()).warnLegacyBrowsers).toEqual(false); + }); -test('does not include raw csp.rules under any property names', async () => { - const { collector } = setupCollector(); - - // It's important that we do not send the value of csp.rules here as it - // can be customized with values that can be identifiable to given - // installs, such as URLs - // - // We use a snapshot here to ensure csp.rules isn't finding its way into the - // payload under some new and unexpected variable name (e.g. cspRules). - expect(await collector.fetch()).toMatchInlineSnapshot(` - Object { - "rulesChangedFromDefault": false, - "strict": true, - "warnLegacyBrowsers": true, - } - `); -}); + test('fetches whether the csp rules have been changed or not', async () => { + const collector = createCspCollector(kbnServer as any); -test('does not arbitrarily fetch other csp configurations (e.g. whitelist only)', async () => { - const { collector, mockConfig } = setupCollector(); + expect((await collector.fetch()).rulesChangedFromDefault).toEqual(false); - mockConfig.get.withArgs('csp.foo').returns('bar'); + updateCsp({ rules: ['not', 'default'] }); + expect((await collector.fetch()).rulesChangedFromDefault).toEqual(true); + }); - expect(await collector.fetch()).not.toHaveProperty('foo'); -}); - -function setupCollector() { - const mockConfig = { get: sinon.stub() }; - mockConfig.get.withArgs('csp.rules').returns(DEFAULT_CSP_RULES); - mockConfig.get.withArgs('csp.strict').returns(true); - mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(true); + test('does not include raw csp rules under any property names', async () => { + const collector = createCspCollector(kbnServer as any); - const mockKbnServer = getMockKbnServer(mockConfig); - - return { mockConfig, collector: createCspCollector(mockKbnServer as Server) }; -} + // It's important that we do not send the value of csp.rules here as it + // can be customized with values that can be identifiable to given + // installs, such as URLs + // + // We use a snapshot here to ensure csp.rules isn't finding its way into the + // payload under some new and unexpected variable name (e.g. cspRules). + expect(await collector.fetch()).toMatchInlineSnapshot(` + Object { + "rulesChangedFromDefault": false, + "strict": true, + "warnLegacyBrowsers": true, + } + `); + }); +}); diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts index 9890aaf187a13..6622ed4bef478 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts @@ -18,7 +18,7 @@ */ import { Server } from 'hapi'; -import { createCSPRuleString, DEFAULT_CSP_RULES } from '../../../../../server/csp'; +import { CspConfig } from '../../../../../../core/server'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export function createCspCollector(server: Server) { @@ -26,18 +26,15 @@ export function createCspCollector(server: Server) { type: 'csp', isReady: () => true, async fetch() { - const config = server.config(); - - // It's important that we do not send the value of csp.rules here as it - // can be customized with values that can be identifiable to given - // installs, such as URLs - const defaultRulesString = createCSPRuleString([...DEFAULT_CSP_RULES]); - const actualRulesString = createCSPRuleString(config.get('csp.rules')); + const { strict, warnLegacyBrowsers, header } = server.newPlatform.setup.core.http.csp; return { - strict: config.get('csp.strict'), - warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'), - rulesChangedFromDefault: defaultRulesString !== actualRulesString, + strict, + warnLegacyBrowsers, + // It's important that we do not send the value of csp.header here as it + // can be customized with values that can be identifiable to given + // installs, such as URLs + rulesChangedFromDefault: header !== CspConfig.DEFAULT.header, }; }, }; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 8f10843a99cbd..bfbb51e1f5f12 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -22,7 +22,6 @@ import os from 'os'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { fromRoot } from '../../../core/server/utils'; -import { DEFAULT_CSP_RULES, DEFAULT_CSP_STRICT, DEFAULT_CSP_WARN_LEGACY_BROWSERS } from '../csp'; const HANDLED_IN_NEW_PLATFORM = Joi.any().description( 'This key is handled in the new platform ONLY' @@ -51,13 +50,7 @@ export default () => exclusive: Joi.boolean().default(false), }).default(), - csp: Joi.object({ - rules: Joi.array() - .items(Joi.string()) - .default(DEFAULT_CSP_RULES), - strict: Joi.boolean().default(DEFAULT_CSP_STRICT), - warnLegacyBrowsers: Joi.boolean().default(DEFAULT_CSP_WARN_LEGACY_BROWSERS), - }).default(), + csp: HANDLED_IN_NEW_PLATFORM, cpu: Joi.object({ cgroup: Joi.object({ diff --git a/src/legacy/server/csp/index.test.ts b/src/legacy/server/csp/index.test.ts deleted file mode 100644 index 1f1ded2b9d266..0000000000000 --- a/src/legacy/server/csp/index.test.ts +++ /dev/null @@ -1,61 +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. - */ - -import { - createCSPRuleString, - DEFAULT_CSP_RULES, - DEFAULT_CSP_STRICT, - DEFAULT_CSP_WARN_LEGACY_BROWSERS, -} from './'; - -// CSP rules aren't strictly additive, so any change can potentially expand or -// restrict the policy in a way we consider a breaking change. For that reason, -// we test the default rules exactly so any change to those rules gets flagged -// for manual review. In otherwords, this test is intentionally fragile to draw -// extra attention if defaults are modified in any way. -// -// A test failure here does not necessarily mean this change cannot be made, -// but any change here should undergo sufficient scrutiny by the Kibana -// security team. -// -// The tests use inline snapshots to make it as easy as possible to identify -// the nature of a change in defaults during a PR review. -test('default CSP rules', () => { - expect(DEFAULT_CSP_RULES).toMatchInlineSnapshot(` - Array [ - "script-src 'unsafe-eval' 'self'", - "worker-src blob: 'self'", - "style-src 'unsafe-inline' 'self'", - ] - `); -}); - -test('CSP strict mode defaults to disabled', () => { - expect(DEFAULT_CSP_STRICT).toBe(false); -}); - -test('CSP legacy browser warning defaults to enabled', () => { - expect(DEFAULT_CSP_WARN_LEGACY_BROWSERS).toBe(true); -}); - -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:"`); -}); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index aee6f10e93dbd..e3189f02bbbf2 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -28,7 +28,6 @@ import { AppBootstrap } from './bootstrap'; import { mergeVariables } from './lib'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { fromRoot } from '../../../core/server/utils'; -import { createCSPRuleString } from '../../server/csp'; export function uiRenderMixin(kbnServer, server, config) { function replaceInjectedVars(request, injectedVars) { @@ -248,9 +247,10 @@ export function uiRenderMixin(kbnServer, server, config) { } ) ); + const { strict, warnLegacyBrowsers, header } = kbnServer.newPlatform.setup.core.http.csp; const response = h.view('ui_app', { - strictCsp: config.get('csp.strict'), + strictCsp: strict, uiPublicUrl: `${basePath}/ui`, bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`, i18n: (id, options) => i18n.translate(id, options), @@ -268,7 +268,7 @@ export function uiRenderMixin(kbnServer, server, config) { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, csp: { - warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'), + warnLegacyBrowsers, }, vars: await replaceInjectedVars( request, @@ -285,8 +285,7 @@ export function uiRenderMixin(kbnServer, server, config) { }, }); - const csp = createCSPRuleString(config.get('csp.rules')); - response.header('content-security-policy', csp); + response.header('content-security-policy', header); return response; } diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index 3a68088359aa5..bf1a40e95b4dc 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -13,7 +13,6 @@ import { initLoggedOutView } from './server/routes/views/logged_out'; import { AuditLogger } from '../../server/lib/audit_logger'; import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { KibanaRequest } from '../../../../src/core/server'; -import { createCSPRuleString } from '../../../../src/legacy/server/csp'; export const security = kibana => new kibana.Plugin({ @@ -158,7 +157,6 @@ export const security = kibana => isSystemAPIRequest: server.plugins.kibana.systemApi.isSystemApiRequest.bind( server.plugins.kibana.systemApi ), - cspRules: createCSPRuleString(config.get('csp.rules')), }); // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 7bcf282ba7376..d355e15f03401 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -44,7 +44,6 @@ export type FeaturesService = Pick; export interface LegacyAPI { serverConfig: { protocol: string; hostname: string; port: number }; isSystemAPIRequest: (request: KibanaRequest) => boolean; - cspRules: string; savedObjects: SavedObjectsLegacyService; auditLogger: { log: (eventType: string, message: string, data?: Record) => void; @@ -165,7 +164,7 @@ export class Plugin { config, authc, authz, - getLegacyAPI: this.getLegacyAPI, + csp: core.http.csp, }); const adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); diff --git a/x-pack/plugins/security/server/routes/authentication/basic.test.ts b/x-pack/plugins/security/server/routes/authentication/basic.test.ts index 8e24f99b1302d..be17b3e29f854 100644 --- a/x-pack/plugins/security/server/routes/authentication/basic.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/basic.test.ts @@ -15,7 +15,6 @@ import { import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; import { Authentication, AuthenticationResult } from '../../authentication'; import { ConfigType } from '../../config'; -import { LegacyAPI } from '../../plugin'; import { defineBasicRoutes } from './basic'; import { @@ -50,7 +49,7 @@ describe('Basic authentication routes', () => { config: { authc: { providers: ['saml'] } } as ConfigType, authc, authz: authorizationMock.create(), - getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + csp: httpServiceMock.createSetupContract().csp, }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index f57fb1d5a7d66..5d5868d4cc593 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -15,7 +15,6 @@ import { import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; import { Authentication, DeauthenticationResult } from '../../authentication'; import { ConfigType } from '../../config'; -import { LegacyAPI } from '../../plugin'; import { defineCommonRoutes } from './common'; import { @@ -50,7 +49,7 @@ describe('Common authentication routes', () => { config: { authc: { providers: ['saml'] } } as ConfigType, authc, authz: authorizationMock.create(), - getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + csp: httpServiceMock.createSetupContract().csp, }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/index.test.ts b/x-pack/plugins/security/server/routes/authentication/index.test.ts index cad370b7837e1..5450dfafa5e49 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.test.ts @@ -27,7 +27,7 @@ describe('Authentication routes', () => { config: { authc: { providers: ['basic'] } } as ConfigType, authc: authenticationMock.create(), authz: authorizationMock.create(), - getLegacyAPI: () => ({ cspRules: 'test-csp-rule' }), + csp: httpServiceMock.createSetupContract().csp, }); const samlRoutePathPredicate = ([{ path }]: [{ path: string }, any]) => diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 21f015cc23b68..6035025564cbf 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -11,13 +11,13 @@ import { defineCommonRoutes } from './common'; import { defineOIDCRoutes } from './oidc'; import { RouteDefinitionParams } from '..'; -export function createCustomResourceResponse(body: string, contentType: string, cspRules: string) { +export function createCustomResourceResponse(body: string, contentType: string, cspHeader: string) { return { body, headers: { 'content-type': contentType, 'cache-control': 'private, no-cache, no-store', - 'content-security-policy': cspRules, + 'content-security-policy': cspHeader, }, statusCode: 200, }; diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 8483630763ae6..ee9c2b46ac878 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -17,13 +17,7 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -export function defineOIDCRoutes({ - router, - logger, - authc, - getLegacyAPI, - basePath, -}: RouteDefinitionParams) { +export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) { // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) { /** @@ -54,7 +48,7 @@ export function defineOIDCRoutes({ `, 'text/html', - getLegacyAPI().cspRules + csp.header ) ); } @@ -82,7 +76,7 @@ export function defineOIDCRoutes({ ); `, 'text/javascript', - getLegacyAPI().cspRules + csp.header ) ); } diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index 73ce800e9d8d7..b30671d15a0c4 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -9,7 +9,6 @@ import { Authentication, AuthenticationResult, SAMLLoginStep } from '../../authe import { defineSAMLRoutes } from './saml'; import { ConfigType } from '../../config'; import { IRouter, RequestHandler, RouteConfig } from '../../../../../../src/core/server'; -import { LegacyAPI } from '../../plugin'; import { elasticsearchServiceMock, @@ -36,7 +35,7 @@ describe('SAML authentication routes', () => { config: { authc: { providers: ['saml'] } } as ConfigType, authc, authz: authorizationMock.create(), - getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + csp: httpServiceMock.createSetupContract().csp, }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 8726c0cce1ae8..e45eae827c858 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -12,13 +12,7 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -export function defineSAMLRoutes({ - router, - logger, - authc, - getLegacyAPI, - basePath, -}: RouteDefinitionParams) { +export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) { router.get( { path: '/api/security/saml/capture-url-fragment', @@ -36,7 +30,7 @@ export function defineSAMLRoutes({ `, 'text/html', - getLegacyAPI().cspRules + csp.header ) ); } @@ -57,7 +51,7 @@ export function defineSAMLRoutes({ ); `, 'text/javascript', - getLegacyAPI().cspRules + csp.header ) ); } diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index 2d3a3154e6499..8a32e6b00bdf4 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -17,11 +17,11 @@ export const routeDefinitionParamsMock = { create: () => ({ router: httpServiceMock.createRouter(), basePath: httpServiceMock.createBasePath(), + csp: httpServiceMock.createSetupContract().csp, logger: loggingServiceMock.create().get(), clusterClient: elasticsearchServiceMock.createClusterClient(), config: { ...ConfigSchema.validate({}), encryptionKey: 'some-enc-key' }, authc: authenticationMock.create(), authz: authorizationMock.create(), - getLegacyAPI: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 756eaa76e2c2e..ade840e7ca495 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -8,7 +8,6 @@ import { CoreSetup, IClusterClient, IRouter, Logger } from '../../../../../src/c import { Authentication } from '../authentication'; import { Authorization } from '../authorization'; import { ConfigType } from '../config'; -import { LegacyAPI } from '../plugin'; import { defineAuthenticationRoutes } from './authentication'; import { defineAuthorizationRoutes } from './authorization'; @@ -22,12 +21,12 @@ import { defineUsersRoutes } from './users'; export interface RouteDefinitionParams { router: IRouter; basePath: CoreSetup['http']['basePath']; + csp: CoreSetup['http']['csp']; logger: Logger; clusterClient: IClusterClient; config: ConfigType; authc: Authentication; authz: Authorization; - getLegacyAPI: () => Pick; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index 9f88d28bc115f..80a25e03ede62 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -17,7 +17,6 @@ import { import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; import { Authentication, AuthenticationResult } from '../../authentication'; import { ConfigType } from '../../config'; -import { LegacyAPI } from '../../plugin'; import { defineChangeUserPasswordRoutes } from './change_password'; import { @@ -77,7 +76,7 @@ describe('Change password', () => { config: { authc: { providers: ['saml'] } } as ConfigType, authc, authz: authorizationMock.create(), - getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + csp: httpServiceMock.createSetupContract().csp, }); const [changePasswordRouteConfig, changePasswordRouteHandler] = router.post.mock.calls[0];