From 614bde927efc34c9d90b0984da3e1f2a51a784b1 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 --- .github/CODEOWNERS | 3 +- .../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 | 11 +- 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 +- 52 files changed, 601 insertions(+), 246 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/.github/CODEOWNERS b/.github/CODEOWNERS index 36a2cda841fa8b..3142e0ff977495 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -80,7 +80,6 @@ /x-pack/plugins/licensing/ @elastic/kibana-platform /packages/kbn-config-schema/ @elastic/kibana-platform /src/legacy/server/config/ @elastic/kibana-platform -/src/legacy/server/csp/ @elastic/kibana-platform /src/legacy/server/http/ @elastic/kibana-platform /src/legacy/server/i18n/ @elastic/kibana-platform /src/legacy/server/logging/ @elastic/kibana-platform @@ -88,12 +87,12 @@ /src/legacy/server/status/ @elastic/kibana-platform # Security +/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform /x-pack/legacy/plugins/security/ @elastic/kibana-security /x-pack/legacy/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security -/src/legacy/server/csp/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security 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 00000000000000..56e6cf35cdd136 --- /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 00000000000000..e3a3d5d712a420 --- /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 00000000000000..e5276991be404f --- /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 00000000000000..c5270c2375dc17 --- /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 00000000000000..3ac48edd374c95 --- /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 00000000000000..59d661593d9402 --- /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 00000000000000..7bf83305613eaf --- /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 25eebf1c06d010..99d4caf40c0d34 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 00000000000000..d757863fdc12da --- /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 00000000000000..fb8188386a3766 --- /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 00000000000000..6216e6d8171360 --- /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 00000000000000..4ab97ad9f665a6 --- /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 00000000000000..aea35f05694483 --- /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 06dcede0f2dfe7..e97ecbcfaf7396 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 00000000000000..41a319748a1c93 --- /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: true }), + 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 00000000000000..45fa8445791b07 --- /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 00000000000000..bb57702a4a2414 --- /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 ae5cb63ad6ff82..a9e320ac5afa50 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 = true; - -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 1ee7e13d5e8511..888313e1478cb7 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 cb7726de4da5a6..912459c83df6e6 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 244b3cca60f31d..994a6cced89145 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 444aa04171dbdd..1668b409050b74 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 a2546709a318ca..8b500caf217dc5 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 caebd768c70e56..faeae0b559b6be 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 94c1982a18c0ac..92217515a22a1c 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 c304958f78bb70..835c5872d51a30 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 201f761701a35c..db2bc117280ca0 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 4c2e57dc69b29c..662cc0bdf2f3a4 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 c07caaa04ba52a..07b60e771d6434 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 6829784e6e0a19..26c65baf955357 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 18e76324ff3093..381ed9da2b62a3 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 e7bc57ea5fb948..725a45f1319922 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 4878fb9ccae19c..6e3e6bfe208a60 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 36e7dc81d47084..395cb605878328 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 9890aaf187a130..6622ed4bef478e 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 a19a39da0f6dd9..398c774f153242 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -22,11 +22,6 @@ import os from 'os'; import { join } from 'path'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getDataPath } from '../../../core/server/path'; // Still used by optimize config schema -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'); export default () => Joi.object({ @@ -52,11 +47,7 @@ export default () => Joi.object({ 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 fbb63bd49bf6fe..00000000000000 --- 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(true); -}); - -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 47e1e9e17c5c96..da67a6dc696b97 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) { @@ -246,9 +245,10 @@ export function uiRenderMixin(kbnServer, server, config) { return { id, plugin, 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), @@ -266,7 +266,7 @@ export function uiRenderMixin(kbnServer, server, config) { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, csp: { - warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'), + warnLegacyBrowsers, }, vars: await replaceInjectedVars( request, @@ -283,8 +283,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 55963ae4b5c3db..e3f9ffd7a4fb6e 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -12,7 +12,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({ id: 'security', @@ -126,7 +125,6 @@ export const security = (kibana) => new kibana.Plugin({ 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 a395278a5143eb..df5be97e7c393b 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -43,7 +43,6 @@ export type FeaturesService = Pick; */ export interface LegacyAPI { isSystemAPIRequest: (request: KibanaRequest) => boolean; - cspRules: string; savedObjects: SavedObjectsLegacyService; auditLogger: { log: (eventType: string, message: string, data?: Record) => void; @@ -164,7 +163,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 8e24f99b1302d4..be17b3e29f8548 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 f57fb1d5a7d668..5d5868d4cc5936 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 cad370b7837e1e..5450dfafa5e497 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 21f015cc23b68c..6035025564cbfe 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 8483630763ae6b..ee9c2b46ac8785 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 c8735f9f87f4a5..b6447273c25590 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 f724d0e7708be4..06acf5283fe971 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 2d3a3154e64990..8a32e6b00bdf4a 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 756eaa76e2c2e6..ade840e7ca4959 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 9f88d28bc115f1..80a25e03ede62a 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];