[7.x] [csp] allow blob styles when running from source (#54991) (#55597)

* [csp] allow blob styles when running from source

* update kbn/pm dist

* add kibanaServer service to saml_api_integration suite

* use common naming

* attempt to use env.packageInfo instead of IS_KIBANA_DISTRIBUTABLE const

* remove mock, clone rules before modifying

* pass env where necessary

* update core api docs

* make env optional in HttpConfig

* add tests for CspConfig changes, base header on this.rules

* fix test snapshot

* make env optional in HttpConfig too

* remove CspConfig.DEFAULT and make env a required constructor arg

* update csp_usage_collector tests

* update core api docs

* fix test name

* rename headerChangedFromDefault back to rulesChangedFromDefault

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Spencer 2020-01-22 17:20:52 -07:00 committed by GitHub
parent 3dc41fbc80
commit d198bb521b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 212 additions and 97 deletions

View file

@ -1,17 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md) &gt; [getStartServices](./kibana-plugin-server.coresetup.getstartservices.md)
## CoreSetup.getStartServices() method
Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed `start`<!-- -->. This should only be used inside handlers registered during `setup` that will only be executed after `start` lifecycle.
<b>Signature:</b>
```typescript
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
```
<b>Returns:</b>
`Promise<[CoreStart, TPluginsStart]>`
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md) &gt; [getStartServices](./kibana-plugin-server.coresetup.getstartservices.md)
## CoreSetup.getStartServices() method
Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed `start`<!-- -->. This should only be used inside handlers registered during `setup` that will only be executed after `start` lifecycle.
<b>Signature:</b>
```typescript
getStartServices(): Promise<[CoreStart, TPluginsStart]>;
```
<b>Returns:</b>
`Promise<[CoreStart, TPluginsStart]>`

View file

@ -1,32 +1,32 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md)
## CoreSetup interface
Context passed to the plugins `setup` method.
<b>Signature:</b>
```typescript
export interface CoreSetup<TPluginsStart extends object = object>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | <code>CapabilitiesSetup</code> | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
| [context](./kibana-plugin-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-server.contextsetup.md) |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) |
| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) |
| [uuid](./kibana-plugin-server.coresetup.uuid.md) | <code>UuidServiceSetup</code> | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) |
## Methods
| Method | Description |
| --- | --- |
| [getStartServices()](./kibana-plugin-server.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed <code>start</code>. This should only be used inside handlers registered during <code>setup</code> that will only be executed after <code>start</code> lifecycle. |
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CoreSetup](./kibana-plugin-server.coresetup.md)
## CoreSetup interface
Context passed to the plugins `setup` method.
<b>Signature:</b>
```typescript
export interface CoreSetup<TPluginsStart extends object = object>
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | <code>CapabilitiesSetup</code> | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) |
| [context](./kibana-plugin-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-server.contextsetup.md) |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) |
| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) |
| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) |
| [uuid](./kibana-plugin-server.coresetup.uuid.md) | <code>UuidServiceSetup</code> | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) |
## Methods
| Method | Description |
| --- | --- |
| [getStartServices()](./kibana-plugin-server.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed <code>start</code>. This should only be used inside handlers registered during <code>setup</code> that will only be executed after <code>start</code> lifecycle. |

View file

@ -20,9 +20,9 @@ The constructor for this class is marked as internal. Third-party code should no
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [DEFAULT](./kibana-plugin-server.cspconfig.default.md) | <code>static</code> | <code>CspConfig</code> | |
| [header](./kibana-plugin-server.cspconfig.header.md) | | <code>string</code> | |
| [rules](./kibana-plugin-server.cspconfig.rules.md) | | <code>string[]</code> | |
| [rulesChangedFromDefault](./kibana-plugin-server.cspconfig.ruleschangedfromdefault.md) | | <code>boolean</code> | |
| [strict](./kibana-plugin-server.cspconfig.strict.md) | | <code>boolean</code> | |
| [warnLegacyBrowsers](./kibana-plugin-server.cspconfig.warnlegacybrowsers.md) | | <code>boolean</code> | |

View file

@ -1,11 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CspConfig](./kibana-plugin-server.cspconfig.md) &gt; [DEFAULT](./kibana-plugin-server.cspconfig.default.md)
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [CspConfig](./kibana-plugin-server.cspconfig.md) &gt; [rulesChangedFromDefault](./kibana-plugin-server.cspconfig.ruleschangedfromdefault.md)
## CspConfig.DEFAULT property
## CspConfig.rulesChangedFromDefault property
<b>Signature:</b>
```typescript
static readonly DEFAULT: CspConfig;
readonly rulesChangedFromDefault: boolean;
```

View file

@ -18,6 +18,7 @@ export interface ICspConfig
| --- | --- | --- |
| [header](./kibana-plugin-server.icspconfig.header.md) | <code>string</code> | The CSP rules in a formatted directives string for use in a <code>Content-Security-Policy</code> header. |
| [rules](./kibana-plugin-server.icspconfig.rules.md) | <code>string[]</code> | The CSP rules used for Kibana. |
| [rulesChangedFromDefault](./kibana-plugin-server.icspconfig.ruleschangedfromdefault.md) | <code>boolean</code> | Flag indicating that the configuraion changes the csp rules from the defaults |
| [strict](./kibana-plugin-server.icspconfig.strict.md) | <code>boolean</code> | Specify whether browsers that do not support CSP should be able to use Kibana. Use <code>true</code> to block and <code>false</code> to allow. |
| [warnLegacyBrowsers](./kibana-plugin-server.icspconfig.warnlegacybrowsers.md) | <code>boolean</code> | Specify whether users with legacy browsers should be warned about their lack of Kibana security compliance. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ICspConfig](./kibana-plugin-server.icspconfig.md) &gt; [rulesChangedFromDefault](./kibana-plugin-server.icspconfig.ruleschangedfromdefault.md)
## ICspConfig.rulesChangedFromDefault property
Flag indicating that the configuraion changes the csp rules from the defaults
<b>Signature:</b>
```typescript
readonly rulesChangedFromDefault: boolean;
```

View file

@ -32,6 +32,7 @@ interface Status {
interface ApiResponseStatus {
name: string;
uuid: string;
running_from_source?: true;
version: {
number: string;
build_hash: string;
@ -58,6 +59,11 @@ export class KbnClientStatus {
});
}
public async isDistributable() {
const status = await this.get();
return !status.running_from_source;
}
/**
* Get the overall/merged state
*/

View file

@ -43639,6 +43639,10 @@ class KbnClientStatus {
path: 'api/status',
});
}
async isDistributable() {
const status = await this.get();
return !status.running_from_source;
}
/**
* Get the overall/merged state
*/

View file

@ -0,0 +1,29 @@
/*
* 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 { Env } from './env';
export function createMockEnv(options: { dist?: boolean } = {}): Env {
return {
// required by CspConfig
packageInfo: {
dist: options.dist ?? true,
},
} as any;
}

View file

@ -18,6 +18,7 @@
*/
import { CspConfig } from '.';
import { createMockEnv } from '../config/env.mock';
// 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,
@ -33,23 +34,10 @@ import { CspConfig } from '.';
// 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": false,
"warnLegacyBrowsers": true,
}
`);
});
test('defaults from config', () => {
expect(new CspConfig()).toMatchInlineSnapshot(`
const cspConfig = new CspConfig(createMockEnv());
expect(cspConfig).toMatchInlineSnapshot(`
CspConfig {
"header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
"rules": Array [
@ -57,6 +45,7 @@ describe('CspConfig', () => {
"worker-src blob: 'self'",
"style-src 'unsafe-inline' 'self'",
],
"rulesChangedFromDefault": false,
"strict": false,
"warnLegacyBrowsers": true,
}
@ -64,7 +53,9 @@ describe('CspConfig', () => {
});
test('creates from partial config', () => {
expect(new CspConfig({ strict: true, warnLegacyBrowsers: false })).toMatchInlineSnapshot(`
const cspConfig = new CspConfig(createMockEnv(), { strict: false, warnLegacyBrowsers: false });
expect(cspConfig).toMatchInlineSnapshot(`
CspConfig {
"header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
"rules": Array [
@ -72,14 +63,15 @@ describe('CspConfig', () => {
"worker-src blob: 'self'",
"style-src 'unsafe-inline' 'self'",
],
"strict": true,
"rulesChangedFromDefault": false,
"strict": false,
"warnLegacyBrowsers": false,
}
`);
});
test('computes header from rules', () => {
const cspConfig = new CspConfig({ rules: ['alpha', 'beta', 'gamma'] });
const cspConfig = new CspConfig(createMockEnv(), { rules: ['alpha', 'beta', 'gamma'] });
expect(cspConfig).toMatchInlineSnapshot(`
CspConfig {
@ -89,6 +81,25 @@ describe('CspConfig', () => {
"beta",
"gamma",
],
"rulesChangedFromDefault": true,
"strict": false,
"warnLegacyBrowsers": true,
}
`);
});
test(`includes blob: style-src if env indicates we're running from source`, () => {
const cspConfig = new CspConfig(createMockEnv({ dist: false }));
expect(cspConfig).toMatchInlineSnapshot(`
CspConfig {
"header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src blob: 'unsafe-inline' 'self'",
"rules": Array [
"script-src 'unsafe-eval' 'self'",
"worker-src blob: 'self'",
"style-src blob: 'unsafe-inline' 'self'",
],
"rulesChangedFromDefault": false,
"strict": false,
"warnLegacyBrowsers": true,
}

View file

@ -18,6 +18,7 @@
*/
import { config } from './config';
import { Env } from '../config';
const DEFAULT_CONFIG = Object.freeze(config.schema.validate({}));
@ -48,6 +49,12 @@ export interface ICspConfig {
* in a `Content-Security-Policy` header.
*/
readonly header: string;
/**
* Flag indicating that the configuraion changes the csp
* rules from the defaults
*/
readonly rulesChangedFromDefault: boolean;
}
/**
@ -55,23 +62,37 @@ export interface ICspConfig {
* @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;
public readonly rulesChangedFromDefault: boolean;
/**
* Returns the default CSP configuration when passed with no config
* @internal
*/
constructor(rawCspConfig: Partial<Omit<ICspConfig, 'header'>> = {}) {
constructor(env: Env, rawCspConfig?: Partial<Omit<ICspConfig, 'header'>>) {
const source = { ...DEFAULT_CONFIG, ...rawCspConfig };
this.rules = source.rules;
this.rules = source.rules.map(rule => {
// if we receive an env, and it indicates that this isn't a distributable, add `blob:` to the style csp rules
if (env && !env.packageInfo.dist && rule.startsWith('style-src ')) {
return rule.replace(/^style-src /, 'style-src blob: ');
}
return rule;
});
this.strict = source.strict;
this.warnLegacyBrowsers = source.warnLegacyBrowsers;
this.header = source.rules.join('; ');
this.header = this.rules.join('; ');
// only check to see if the csp values are customized when `rawCspConfig` was received.
if (!rawCspConfig) {
this.rulesChangedFromDefault = false;
} else {
const defaultCsp = new CspConfig(env);
this.rulesChangedFromDefault = defaultCsp.header !== this.header;
}
}
}

View file

@ -22,6 +22,7 @@ import { hostname } from 'os';
import { CspConfigType, CspConfig, ICspConfig } from '../csp';
import { SslConfig, sslSchema } from './ssl_config';
import { Env } from '../config';
const validBasePathRegex = /(^$|^\/.*[^\/]$)/;
const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@ -148,7 +149,7 @@ export class HttpConfig {
/**
* @internal
*/
constructor(rawHttpConfig: HttpConfigType, rawCspConfig: CspConfigType) {
constructor(rawHttpConfig: HttpConfigType, rawCspConfig: CspConfigType, env: Env) {
this.autoListen = rawHttpConfig.autoListen;
this.host = rawHttpConfig.host;
this.port = rawHttpConfig.port;
@ -162,7 +163,7 @@ export class HttpConfig {
this.rewriteBasePath = rawHttpConfig.rewriteBasePath;
this.ssl = new SslConfig(rawHttpConfig.ssl || {});
this.compression = rawHttpConfig.compression;
this.csp = new CspConfig(rawCspConfig);
this.csp = new CspConfig(env, rawCspConfig);
this.xsrf = rawHttpConfig.xsrf;
}
}

View file

@ -21,6 +21,7 @@ import { Server } from 'hapi';
import { CspConfig } from '../csp';
import { mockRouter } from './router/router.mock';
import { configMock } from '../config/config.mock';
import { createMockEnv } from '../config/env.mock';
import { InternalHttpServiceSetup } from './types';
import { HttpService } from './http_service';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
@ -61,7 +62,7 @@ const createSetupContractMock = () => {
registerOnPreResponse: jest.fn(),
createRouter: jest.fn().mockImplementation(() => mockRouter.create({})),
basePath: createBasePathMock(),
csp: CspConfig.DEFAULT,
csp: new CspConfig(createMockEnv()),
auth: {
get: jest.fn(),
isAuthenticated: jest.fn(),

View file

@ -70,7 +70,7 @@ export class HttpService implements CoreService<InternalHttpServiceSetup, HttpSe
this.config$ = combineLatest([
configService.atPath<HttpConfigType>(httpConfig.path),
configService.atPath<CspConfigType>(cspConfig.path),
]).pipe(map(([http, csp]) => new HttpConfig(http, csp)));
]).pipe(map(([http, csp]) => new HttpConfig(http, csp, env)));
this.httpServer = new HttpServer(logger, 'Kibana');
this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server'));
}

View file

@ -29,6 +29,7 @@ import { defaultValidationErrorHandler, HapiValidationError, getServerOptions }
import { HttpServer } from './http_server';
import { HttpConfig, config } from './http_config';
import { Router } from './router';
import { createMockEnv } from '../config/env.mock';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { ByteSizeValue } from '@kbn/config-schema';
@ -120,7 +121,8 @@ describe('getServerOptions', () => {
certificate: 'some-certificate-path',
},
}),
{} as any
{} as any,
createMockEnv()
);
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
@ -149,7 +151,8 @@ describe('getServerOptions', () => {
clientAuthentication: 'required',
},
}),
{} as any
{} as any,
createMockEnv()
);
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`

View file

@ -86,7 +86,7 @@ export class LegacyService implements CoreService {
public legacyInternals?: ILegacyInternals;
constructor(private readonly coreContext: CoreContext) {
const { logger, configService } = coreContext;
const { logger, configService, env } = coreContext;
this.log = logger.get('legacy-service');
this.devConfig$ = configService
@ -95,7 +95,7 @@ export class LegacyService implements CoreService {
this.httpConfig$ = combineLatest(
configService.atPath<HttpConfigType>(httpConfig.path),
configService.atPath<CspConfigType>(cspConfig.path)
).pipe(map(([http, csp]) => new HttpConfig(http, csp)));
).pipe(map(([http, csp]) => new HttpConfig(http, csp, env)));
}
public async discoverPlugins(): Promise<LegacyServiceDiscoverPlugins> {

View file

@ -33,6 +33,7 @@ import { capabilitiesServiceMock } from './capabilities/capabilities_service.moc
export { httpServerMock } from './http/http_server.mocks';
export { sessionStorageMock } from './http/cookie_session_storage.mocks';
export { configServiceMock } from './config/config_service.mock';
import { createMockEnv } from './config/env.mock';
export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
export { httpServiceMock } from './http/http_service.mock';
export { loggingServiceMock } from './logging/logging_service.mock';
@ -97,7 +98,7 @@ function createCoreSetupMock() {
registerOnPostAuth: httpService.registerOnPostAuth,
registerOnPreResponse: httpService.registerOnPreResponse,
basePath: httpService.basePath,
csp: CspConfig.DEFAULT,
csp: new CspConfig(createMockEnv()),
isTlsEnabled: httpService.isTlsEnabled,
createRouter: jest.fn(),
registerRouteHandlerContext: jest.fn(),

View file

@ -583,14 +583,14 @@ export interface CoreStart {
// @public
export class CspConfig implements ICspConfig {
// @internal
constructor(rawCspConfig?: Partial<Omit<ICspConfig, 'header'>>);
// (undocumented)
static readonly DEFAULT: CspConfig;
constructor(env: Env, rawCspConfig?: Partial<Omit<ICspConfig, 'header'>>);
// (undocumented)
readonly header: string;
// (undocumented)
readonly rules: string[];
// (undocumented)
readonly rulesChangedFromDefault: boolean;
// (undocumented)
readonly strict: boolean;
// (undocumented)
readonly warnLegacyBrowsers: boolean;
@ -774,6 +774,7 @@ export type IContextProvider<THandler extends HandlerFunction<any>, TContextName
export interface ICspConfig {
readonly header: string;
readonly rules: string[];
readonly rulesChangedFromDefault: boolean;
readonly strict: boolean;
readonly warnLegacyBrowsers: boolean;
}

View file

@ -18,6 +18,8 @@
*/
import { CspConfig, ICspConfig } from '../../../../../../core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { createMockEnv } from '../../../../../../core/server/config/env.mock';
import { createCspCollector } from './csp_collector';
const createMockKbnServer = () => ({
@ -25,7 +27,7 @@ const createMockKbnServer = () => ({
setup: {
core: {
http: {
csp: new CspConfig(),
csp: new CspConfig(createMockEnv()),
},
},
},
@ -36,7 +38,7 @@ describe('csp collector', () => {
let kbnServer: ReturnType<typeof createMockKbnServer>;
function updateCsp(config: Partial<ICspConfig>) {
kbnServer.newPlatform.setup.core.http.csp = new CspConfig(config);
kbnServer.newPlatform.setup.core.http.csp = new CspConfig(createMockEnv(), config);
}
beforeEach(() => {

View file

@ -18,7 +18,6 @@
*/
import { Server } from 'hapi';
import { CspConfig } from '../../../../../../core/server';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
export function createCspCollector(server: Server) {
@ -26,7 +25,11 @@ export function createCspCollector(server: Server) {
type: 'csp',
isReady: () => true,
async fetch() {
const { strict, warnLegacyBrowsers, header } = server.newPlatform.setup.core.http.csp;
const {
strict,
warnLegacyBrowsers,
rulesChangedFromDefault,
} = server.newPlatform.setup.core.http.csp;
return {
strict,
@ -34,7 +37,7 @@ export function createCspCollector(server: Server) {
// 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,
rulesChangedFromDefault,
};
},
};

View file

@ -18,6 +18,7 @@
*/
import { wrapAuthConfig } from '../../wrap_auth_config';
import { IS_KIBANA_DISTRIBUTABLE } from '../../../../utils/artifact_type';
const matchSnapshot = /-SNAPSHOT$/;
@ -35,6 +36,8 @@ export function registerStatusApi(kbnServer, server, config) {
return {
name: config.get('server.name'),
uuid: config.get('server.uuid'),
// flag to help tests know that kibana is running from source, not included in distributable response
...(IS_KIBANA_DISTRIBUTABLE ? {} : { running_from_source: true }),
version: {
number: config.get('pkg.version').replace(matchSnapshot, ''),
build_hash: config.get('pkg.buildSha'),

View file

@ -21,6 +21,7 @@ import expect from '@kbn/expect';
export default function({ getService }) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
describe('csp smoke test', () => {
it('app response sends content security policy headers', async () => {
@ -36,11 +37,12 @@ export default function({ getService }) {
})
);
const isDist = await kibanaServer.status.isDistributable();
const entries = Array.from(parsed.entries());
expect(entries).to.eql([
['script-src', ["'unsafe-eval'", "'self'"]],
['worker-src', ['blob:', "'self'"]],
['style-src', ["'unsafe-inline'", "'self'"]],
['style-src', [...(isDist ? [] : ['blob:']), "'unsafe-inline'", "'self'"]],
]);
});
});

View file

@ -15,6 +15,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function({ getService }: FtrProviderContext) {
const supertest = getService('supertestWithoutAuth');
const config = getService('config');
const kibanaServer = getService('kibanaServer');
describe('OpenID Connect Implicit Flow authentication', () => {
describe('finishing handshake', () => {
@ -56,12 +57,17 @@ export default function({ getService }: FtrProviderContext) {
});
await (dom.window as Record<string, any>).__isScriptExecuted__;
const isDist = await kibanaServer.status.isDistributable();
// Check that proxy page is returned with proper headers.
expect(response.headers['content-type']).to.be('text/html; charset=utf-8');
expect(response.headers['cache-control']).to.be('private, no-cache, no-store');
expect(response.headers['content-security-policy']).to.be(
`script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
[
`script-src 'unsafe-eval' 'self';`,
`worker-src blob: 'self';`,
`style-src ${isDist ? '' : 'blob: '}'unsafe-inline' 'self'`,
].join(' ')
);
// Check that script that forwards URL fragment worked correctly.

View file

@ -17,6 +17,7 @@ export default function({ getService }: FtrProviderContext) {
const randomness = getService('randomness');
const supertest = getService('supertestWithoutAuth');
const config = getService('config');
const kibanaServer = getService('kibanaServer');
const kibanaServerConfig = config.get('servers.kibana');
@ -137,12 +138,17 @@ export default function({ getService }: FtrProviderContext) {
});
await (dom.window as Record<string, any>).__isScriptExecuted__;
const isDist = await kibanaServer.status.isDistributable();
// Check that proxy page is returned with proper headers.
expect(response.headers['content-type']).to.be('text/html; charset=utf-8');
expect(response.headers['cache-control']).to.be('private, no-cache, no-store');
expect(response.headers['content-security-policy']).to.be(
`script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
[
`script-src 'unsafe-eval' 'self';`,
`worker-src blob: 'self';`,
`style-src ${isDist ? '' : 'blob: '}'unsafe-inline' 'self'`,
].join(' ')
);
// Check that script that forwards URL fragment worked correctly.

View file

@ -20,6 +20,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) {
testFiles: [require.resolve('./apis')],
servers: xPackAPITestsConfig.get('servers'),
services: {
kibanaServer: kibanaAPITestsConfig.get('services.kibanaServer'),
randomness: kibanaAPITestsConfig.get('services.randomness'),
legacyEs: kibanaAPITestsConfig.get('services.legacyEs'),
supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'),