mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Warn legacy browsers that do not support Content Security Policy (#29957)
* csp: warn legacy browsers that do not support CSP The new csp.warnLegacyBrowsers configuration is enabled by default, and it shows a warning message to any legacy browser when they access Kibana to indicate that they are not enforcing the basic security protections of the current install. The protections check is the same as csp.strict, so this feature is designed to be used as an alternative to aid in BWC. When csp.strict is enabled, warnLegacyBrowsers is effectively ignored. * fix ChromeService tests * more test fixes * csp injectvars in legacy test bundle * update warning text and make it translatable * no need to warn in legacy browser unit tests * tests for chrome legacy browser warning * document legacy browser warning breaking change * update csp warning toast message * add period, remove dev code
This commit is contained in:
parent
bf6f419c28
commit
7094548bca
12 changed files with 198 additions and 26 deletions
|
@ -197,3 +197,12 @@ dependent on an unspecified port set to 9200, `:9200` will have to be appended t
|
|||
|
||||
*Impact:* Users with `elasticsearch.ssl.verify` set should use `elasticsearch.ssl.verificationMode` instead.
|
||||
Previously set `elasticsearch.ssl.verify` is equal to `elasticsearch.ssl.verificationMode: full`.
|
||||
|
||||
[float]
|
||||
=== Legacy browsers (namely IE11) will see a security warning message whenever they load Kibana
|
||||
*Details:* Kibana now has a Content Security Policy, but it's only effective if browsers enforce it, and since older
|
||||
browsers like Internet Explorer 11 do not support CSP, we show them a warning message whenever they load Kibana.
|
||||
|
||||
*Impact:* Nothing needs to be done necessarily, but if you don't need to support legacy browsers like IE11, we recommend
|
||||
that you set `csp.strict: true` in your kibana.yml to block access to those browsers entirely. If your organization requires
|
||||
users to use IE11, you might like to disable the warning entirely with `csp.warnLegacyBrowsers: false` in your kibana.yml.
|
||||
|
|
|
@ -23,6 +23,8 @@ you'll need to update your `kibana.yml` file. You can also enable SSL and set a
|
|||
|
||||
`csp.strict:`:: *Default: `false`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable support for older, less safe browsers like Internet Explorer.
|
||||
|
||||
`csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after loading Kibana to any browser that does not enforce even rudimentary CSP rules, though Kibana is still accessible. This configuration is effectively ignored when `csp.strict` is enabled.
|
||||
|
||||
`elasticsearch.customHeaders:`:: *Default: `{}`* Header names and values to send to Elasticsearch. Any custom headers
|
||||
cannot be overwritten by client-side headers, regardless of the `elasticsearch.requestHeadersWhitelist` configuration.
|
||||
|
||||
|
|
|
@ -29,15 +29,74 @@ const store = new Map();
|
|||
|
||||
import { ChromeService } from './chrome_service';
|
||||
|
||||
function defaultStartDeps(): any {
|
||||
return {
|
||||
notifications: {
|
||||
toasts: {
|
||||
addWarning: jest.fn(),
|
||||
},
|
||||
},
|
||||
injectedMetadata: {
|
||||
injectedMetadataStartContract: true,
|
||||
getCspConfig: jest.fn().mockReturnValue({ warnLegacyBrowsers: true }),
|
||||
getKibanaVersion: jest.fn().mockReturnValue('kibanaVersion'),
|
||||
getLegacyMetadata: jest.fn().mockReturnValue({
|
||||
uiSettings: {
|
||||
defaults: { legacyInjectedUiSettingDefaults: true },
|
||||
user: { legacyInjectedUiSettingUserValues: true },
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store.clear();
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', () => {
|
||||
const service = new ChromeService({ browserSupportsCsp: false });
|
||||
const startDeps = defaultStartDeps();
|
||||
startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
|
||||
service.start(startDeps);
|
||||
expect(startDeps.notifications.toasts.addWarning).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"Your browser does not meet the security requirements for Kibana.",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not add legacy browser warning if browser supports CSP', () => {
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const startDeps = defaultStartDeps();
|
||||
startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
|
||||
service.start(startDeps);
|
||||
expect(startDeps.notifications.toasts.addWarning).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not add legacy browser warning if warnLegacyBrowsers is disabled', () => {
|
||||
const service = new ChromeService({ browserSupportsCsp: false });
|
||||
const startDeps = defaultStartDeps();
|
||||
startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: false });
|
||||
service.start(startDeps);
|
||||
expect(startDeps.notifications.toasts.addWarning).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('brand', () => {
|
||||
it('updates/emits the brand as it changes', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getBrand$()
|
||||
.pipe(toArray())
|
||||
|
@ -70,8 +129,8 @@ Array [
|
|||
|
||||
describe('visibility', () => {
|
||||
it('updates/emits the visibility', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getIsVisible$()
|
||||
.pipe(toArray())
|
||||
|
@ -95,8 +154,8 @@ Array [
|
|||
it('always emits false if embed query string is in hash when started', async () => {
|
||||
window.history.pushState(undefined, '', '#/home?a=b&embed=true');
|
||||
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getIsVisible$()
|
||||
.pipe(toArray())
|
||||
|
@ -120,8 +179,8 @@ Array [
|
|||
|
||||
describe('is collapsed', () => {
|
||||
it('updates/emits isCollapsed', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getIsCollapsed$()
|
||||
.pipe(toArray())
|
||||
|
@ -143,8 +202,8 @@ Array [
|
|||
});
|
||||
|
||||
it('only stores true in localStorage', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
|
||||
start.setIsCollapsed(true);
|
||||
expect(store.size).toBe(1);
|
||||
|
@ -156,8 +215,8 @@ Array [
|
|||
|
||||
describe('application classes', () => {
|
||||
it('updates/emits the application classes', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getApplicationClasses$()
|
||||
.pipe(toArray())
|
||||
|
@ -208,8 +267,8 @@ Array [
|
|||
|
||||
describe('breadcrumbs', () => {
|
||||
it('updates/emits the current set of breadcrumbs', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getBreadcrumbs$()
|
||||
.pipe(toArray())
|
||||
|
@ -250,8 +309,8 @@ Array [
|
|||
|
||||
describe('help extension', () => {
|
||||
it('updates/emits the current help extension', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = start
|
||||
.getHelpExtension$()
|
||||
.pipe(toArray())
|
||||
|
@ -274,8 +333,8 @@ Array [
|
|||
|
||||
describe('stop', () => {
|
||||
it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
const promise = Rx.combineLatest(
|
||||
start.getBrand$(),
|
||||
start.getApplicationClasses$(),
|
||||
|
@ -290,8 +349,8 @@ describe('stop', () => {
|
|||
});
|
||||
|
||||
it('completes immediately if service already stopped', async () => {
|
||||
const service = new ChromeService();
|
||||
const start = service.start();
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const start = service.start(defaultStartDeps());
|
||||
service.stop();
|
||||
|
||||
await expect(
|
||||
|
|
|
@ -19,8 +19,11 @@
|
|||
|
||||
import * as Url from 'url';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import * as Rx from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { InjectedMetadataStartContract } from '../injected_metadata';
|
||||
import { NotificationsStartContract } from '../notifications';
|
||||
|
||||
const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed';
|
||||
|
||||
|
@ -42,10 +45,24 @@ export interface Breadcrumb {
|
|||
|
||||
export type HelpExtension = (element: HTMLDivElement) => (() => void);
|
||||
|
||||
interface ConstructorParams {
|
||||
browserSupportsCsp: boolean;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
injectedMetadata: InjectedMetadataStartContract;
|
||||
notifications: NotificationsStartContract;
|
||||
}
|
||||
|
||||
export class ChromeService {
|
||||
private readonly stop$ = new Rx.ReplaySubject(1);
|
||||
private readonly browserSupportsCsp: boolean;
|
||||
|
||||
public start() {
|
||||
public constructor({ browserSupportsCsp }: ConstructorParams) {
|
||||
this.browserSupportsCsp = browserSupportsCsp;
|
||||
}
|
||||
|
||||
public start({ injectedMetadata, notifications }: StartDeps) {
|
||||
const FORCE_HIDDEN = isEmbedParamInHash();
|
||||
|
||||
const brand$ = new Rx.BehaviorSubject<Brand>({});
|
||||
|
@ -55,6 +72,14 @@ export class ChromeService {
|
|||
const helpExtension$ = new Rx.BehaviorSubject<HelpExtension | undefined>(undefined);
|
||||
const breadcrumbs$ = new Rx.BehaviorSubject<Breadcrumb[]>([]);
|
||||
|
||||
if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
|
||||
notifications.toasts.addWarning(
|
||||
i18n.translate('core.chrome.legacyBrowserWarning', {
|
||||
defaultMessage: 'Your browser does not meet the security requirements for Kibana.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Set the brand configuration. Normally the `logo` property will be rendered as the
|
||||
|
|
|
@ -123,7 +123,12 @@ jest.spyOn(CoreSystem.prototype, 'stop');
|
|||
|
||||
const defaultCoreSystemParams = {
|
||||
rootDomElement: document.createElement('div'),
|
||||
injectedMetadata: {} as any,
|
||||
browserSupportsCsp: true,
|
||||
injectedMetadata: {
|
||||
csp: {
|
||||
warnLegacyBrowsers: true,
|
||||
},
|
||||
} as any,
|
||||
requireLegacyFiles: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -195,6 +200,17 @@ describe('constructor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('passes browserSupportsCsp to ChromeService', () => {
|
||||
new CoreSystem({
|
||||
...defaultCoreSystemParams,
|
||||
});
|
||||
|
||||
expect(MockChromeService).toHaveBeenCalledTimes(1);
|
||||
expect(MockChromeService).toHaveBeenCalledWith({
|
||||
browserSupportsCsp: expect.any(Boolean),
|
||||
});
|
||||
});
|
||||
|
||||
it('passes injectedMetadata, rootDomElement, and a stopCoreSystem function to FatalErrorsService', () => {
|
||||
const rootDomElement = document.createElement('div');
|
||||
const injectedMetadata = { injectedMetadata: true } as any;
|
||||
|
@ -380,7 +396,10 @@ describe('#start()', () => {
|
|||
startCore();
|
||||
const [mockInstance] = MockChromeService.mock.instances;
|
||||
expect(mockInstance.start).toHaveBeenCalledTimes(1);
|
||||
expect(mockInstance.start).toHaveBeenCalledWith();
|
||||
expect(mockInstance.start).toHaveBeenCalledWith({
|
||||
notifications: mockNotificationStartContract,
|
||||
injectedMetadata: mockInjectedMetadataStartContract,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns start contract', () => {
|
||||
|
|
|
@ -31,6 +31,7 @@ import { UiSettingsService } from './ui_settings';
|
|||
|
||||
interface Params {
|
||||
rootDomElement: HTMLElement;
|
||||
browserSupportsCsp: boolean;
|
||||
injectedMetadata: InjectedMetadataParams['injectedMetadata'];
|
||||
requireLegacyFiles: LegacyPlatformParams['requireLegacyFiles'];
|
||||
useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness'];
|
||||
|
@ -58,7 +59,13 @@ export class CoreSystem {
|
|||
private readonly legacyPlatformTargetDomElement: HTMLDivElement;
|
||||
|
||||
constructor(params: Params) {
|
||||
const { rootDomElement, injectedMetadata, requireLegacyFiles, useLegacyTestHarness } = params;
|
||||
const {
|
||||
rootDomElement,
|
||||
browserSupportsCsp,
|
||||
injectedMetadata,
|
||||
requireLegacyFiles,
|
||||
useLegacyTestHarness,
|
||||
} = params;
|
||||
|
||||
this.rootDomElement = rootDomElement;
|
||||
|
||||
|
@ -84,7 +91,7 @@ export class CoreSystem {
|
|||
this.loadingCount = new LoadingCountService();
|
||||
this.basePath = new BasePathService();
|
||||
this.uiSettings = new UiSettingsService();
|
||||
this.chrome = new ChromeService();
|
||||
this.chrome = new ChromeService({ browserSupportsCsp });
|
||||
|
||||
this.legacyPlatformTargetDomElement = document.createElement('div');
|
||||
this.legacyPlatform = new LegacyPlatformService({
|
||||
|
@ -114,7 +121,10 @@ export class CoreSystem {
|
|||
injectedMetadata,
|
||||
basePath,
|
||||
});
|
||||
const chrome = this.chrome.start();
|
||||
const chrome = this.chrome.start({
|
||||
injectedMetadata,
|
||||
notifications,
|
||||
});
|
||||
|
||||
this.legacyPlatform.start({
|
||||
i18n,
|
||||
|
|
|
@ -43,6 +43,39 @@ describe('#getKibanaBuildNumber', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('start.getCspConfig()', () => {
|
||||
it('returns injectedMetadata.csp', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
csp: {
|
||||
warnLegacyBrowsers: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const contract = injectedMetadata.start();
|
||||
expect(contract.getCspConfig()).toEqual({
|
||||
warnLegacyBrowsers: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('csp config is frozen', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
injectedMetadata: {
|
||||
csp: {
|
||||
warnLegacyBrowsers: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const csp = injectedMetadata.start().getCspConfig();
|
||||
expect(() => {
|
||||
// @ts-ignore TS knows this shouldn't be possible
|
||||
csp.warnLegacyBrowsers = false;
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start.getLegacyMetadata()', () => {
|
||||
it('returns injectedMetadata.legacyMetadata', () => {
|
||||
const injectedMetadata = new InjectedMetadataService({
|
||||
|
|
|
@ -26,6 +26,9 @@ export interface InjectedMetadataParams {
|
|||
version: string;
|
||||
buildNumber: number;
|
||||
basePath: string;
|
||||
csp: {
|
||||
warnLegacyBrowsers: boolean;
|
||||
};
|
||||
vars: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
@ -70,6 +73,10 @@ export class InjectedMetadataService {
|
|||
return this.getKibanaVersion();
|
||||
},
|
||||
|
||||
getCspConfig: () => {
|
||||
return this.state.csp;
|
||||
},
|
||||
|
||||
getLegacyMetadata: () => {
|
||||
return this.state.legacyMetadata;
|
||||
},
|
||||
|
|
|
@ -55,6 +55,9 @@ new CoreSystem({
|
|||
user: {}
|
||||
}
|
||||
},
|
||||
csp: {
|
||||
warnLegacyBrowsers: false,
|
||||
},
|
||||
vars: {
|
||||
kbnIndex: '.kibana',
|
||||
esShardTimeout: 1500,
|
||||
|
|
|
@ -57,6 +57,7 @@ export default () => Joi.object({
|
|||
csp: Joi.object({
|
||||
rules: Joi.array().items(Joi.string()).default(DEFAULT_CSP_RULES),
|
||||
strict: Joi.boolean().default(false),
|
||||
warnLegacyBrowsers: Joi.boolean().default(true),
|
||||
}).default(),
|
||||
|
||||
cpu: Joi.object({
|
||||
|
|
|
@ -44,6 +44,7 @@ i18n.load(injectedMetadata.i18n.translationsUrl)
|
|||
const coreSystem = new CoreSystem({
|
||||
injectedMetadata,
|
||||
rootDomElement: document.body,
|
||||
browserSupportsCsp: !window.__kbnCspNotEnforced__,
|
||||
requireLegacyFiles: () => {
|
||||
${bundle.getRequires().join('\n ')}
|
||||
}
|
||||
|
|
|
@ -231,6 +231,9 @@ export function uiRenderMixin(kbnServer, server, config) {
|
|||
i18n: {
|
||||
translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`,
|
||||
},
|
||||
csp: {
|
||||
warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'),
|
||||
},
|
||||
vars: await replaceInjectedVars(
|
||||
request,
|
||||
mergeVariables(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue