-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Allow additive csp configuration #102059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow additive csp configuration #102059
Changes from 12 commits
80ecded
73890df
303d60a
b3d60b2
ee9f805
c8eccd5
d531ac0
dca375f
4b45111
ca5616d
2188b0e
0d3bd2c
400b2d1
e1eb1b5
42c81ec
227c5b7
592a119
383aa95
ec383c3
8a28a62
c74ada5
59303f4
3d7947c
3609981
966fc55
04a4f59
e9590d4
fff64d8
46681a3
596db0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!-- Do not edit this file. It is automatically generated by API Documenter. --> | ||
|
|
||
| [Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CspConfig](./kibana-plugin-core-server.cspconfig.md) > ["\#private"](./kibana-plugin-core-server.cspconfig.__private_.md) | ||
|
|
||
| ## CspConfig."\#private" property | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| #private; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,28 +7,56 @@ | |
| */ | ||
|
|
||
| import { TypeOf, schema } from '@kbn/config-schema'; | ||
| import { ServiceConfigDescriptor } from '../internal_types'; | ||
|
|
||
| const configSchema = schema.object( | ||
| { | ||
| rules: schema.maybe(schema.arrayOf(schema.string())), | ||
| script_src: schema.arrayOf(schema.string(), { defaultValue: [] }), | ||
|
pgayvallet marked this conversation as resolved.
Outdated
|
||
| worker_src: schema.arrayOf(schema.string(), { defaultValue: [] }), | ||
| style_src: schema.arrayOf(schema.string(), { defaultValue: [] }), | ||
|
pgayvallet marked this conversation as resolved.
Outdated
|
||
| strict: schema.boolean({ defaultValue: true }), | ||
| warnLegacyBrowsers: schema.boolean({ defaultValue: true }), | ||
| disableEmbedding: schema.oneOf([schema.literal<boolean>(false)], { defaultValue: false }), | ||
| }, | ||
| { | ||
| validate: (cspConfig) => { | ||
| if ( | ||
| cspConfig.rules && | ||
| (cspConfig.script_src.length || cspConfig.worker_src.length || cspConfig.style_src.length) | ||
| ) { | ||
| return `"csp.rules" cannot be used when specifying per-directive additions such as "script_src", "worker_src" or "style_src"`; | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forbidding both usages simultaneously was what we talked about. Technically we could allow it, as the new |
||
| }, | ||
| } | ||
| ); | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
| export type CspConfigType = TypeOf<typeof config.schema>; | ||
| export type CspConfigType = TypeOf<typeof configSchema>; | ||
|
|
||
| export const config = { | ||
| export const config: ServiceConfigDescriptor<CspConfigType> = { | ||
| // 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 }), | ||
| disableEmbedding: schema.oneOf([schema.literal<boolean>(false)], { defaultValue: false }), | ||
| }), | ||
| schema: configSchema, | ||
| deprecations: () => [ | ||
| (rawConfig, fromPath, addDeprecation) => { | ||
| const cspConfig = rawConfig[fromPath]; | ||
| if (cspConfig?.rules) { | ||
| addDeprecation({ | ||
| message: | ||
| 'csp.rules is deprecated in favor of directive specific configuration. ' + | ||
| 'Please use `csp.script_src`, `csp.worker_src` and `csp.style_src` instead', | ||
|
pgayvallet marked this conversation as resolved.
Outdated
|
||
| correctiveActions: { | ||
| manualSteps: [ | ||
| `Remove "csp.rules" from the Kibana config file"`, | ||
|
pgayvallet marked this conversation as resolved.
Outdated
|
||
| `Add directive specific configurations to the config file, using "csp.script_src", "csp.worker_src" and "csp.style_src"`, | ||
|
pgayvallet marked this conversation as resolved.
Outdated
|
||
| ], | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| export const FRAME_ANCESTORS_RULE = `frame-ancestors 'self'`; // only used by CspConfig when embedding is disabled | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
| */ | ||
|
|
||
| import { CspConfig } from './csp_config'; | ||
| import { FRAME_ANCESTORS_RULE } from './config'; | ||
| import { config as cspConfig, CspConfigType } from './config'; | ||
|
|
||
| // 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, | ||
|
|
@@ -23,6 +23,12 @@ import { FRAME_ANCESTORS_RULE } from './config'; | |
| // the nature of a change in defaults during a PR review. | ||
|
|
||
| describe('CspConfig', () => { | ||
| let defaultConfig: CspConfigType; | ||
|
|
||
| beforeEach(() => { | ||
| defaultConfig = cspConfig.schema.validate({}); | ||
| }); | ||
|
|
||
| test('DEFAULT', () => { | ||
| expect(CspConfig.DEFAULT).toMatchInlineSnapshot(` | ||
| CspConfig { | ||
|
|
@@ -40,50 +46,116 @@ describe('CspConfig', () => { | |
| }); | ||
|
|
||
| test('defaults from config', () => { | ||
| expect(new CspConfig()).toEqual(CspConfig.DEFAULT); | ||
| expect(new CspConfig(defaultConfig)).toEqual(CspConfig.DEFAULT); | ||
| }); | ||
|
|
||
| describe('partial config', () => { | ||
| test('allows "rules" to be set and changes header', () => { | ||
| const rules = ['foo', 'bar']; | ||
| const config = new CspConfig({ rules }); | ||
| const rules = [`foo 'self'`, `bar 'self'`]; | ||
| const config = new CspConfig({ ...defaultConfig, rules }); | ||
| expect(config.rules).toEqual(rules); | ||
| expect(config.header).toMatchInlineSnapshot(`"foo; bar"`); | ||
| expect(config.header).toMatchInlineSnapshot(`"foo 'self'; bar 'self'"`); | ||
| }); | ||
|
|
||
| test('allows "strict" to be set', () => { | ||
| const config = new CspConfig({ strict: false }); | ||
| const config = new CspConfig({ ...defaultConfig, strict: false }); | ||
| expect(config.strict).toEqual(false); | ||
| expect(config.strict).not.toEqual(CspConfig.DEFAULT.strict); | ||
| }); | ||
|
|
||
| test('allows "warnLegacyBrowsers" to be set', () => { | ||
| const warnLegacyBrowsers = false; | ||
| const config = new CspConfig({ warnLegacyBrowsers }); | ||
| const config = new CspConfig({ ...defaultConfig, warnLegacyBrowsers }); | ||
| expect(config.warnLegacyBrowsers).toEqual(warnLegacyBrowsers); | ||
| expect(config.warnLegacyBrowsers).not.toEqual(CspConfig.DEFAULT.warnLegacyBrowsers); | ||
| }); | ||
|
|
||
| test('allows "worker_src" to be set and changes header', () => { | ||
| const config = new CspConfig({ | ||
| ...defaultConfig, | ||
| rules: [], | ||
| worker_src: ['foo', 'bar'], | ||
|
Comment on lines
+73
to
+77
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests are effectively mostly testing the |
||
| }); | ||
| expect(config.rules).toEqual([`worker-src foo bar`]); | ||
| expect(config.header).toEqual(`worker-src foo bar`); | ||
| }); | ||
|
|
||
| test('allows "style_src" to be set and changes header', () => { | ||
| const config = new CspConfig({ | ||
| ...defaultConfig, | ||
| rules: [], | ||
| style_src: ['foo', 'bar'], | ||
| }); | ||
| expect(config.rules).toEqual([`style-src foo bar`]); | ||
| expect(config.header).toEqual(`style-src foo bar`); | ||
| }); | ||
|
|
||
| test('allows "script_src" to be set and changes header', () => { | ||
| const config = new CspConfig({ | ||
| ...defaultConfig, | ||
| rules: [], | ||
| script_src: ['foo', 'bar'], | ||
| }); | ||
| expect(config.rules).toEqual([`script-src foo bar`]); | ||
| expect(config.header).toEqual(`script-src foo bar`); | ||
| }); | ||
|
|
||
| test('allows all directives to be set and changes header', () => { | ||
| const config = new CspConfig({ | ||
| ...defaultConfig, | ||
| rules: [], | ||
| script_src: ['script', 'foo'], | ||
| worker_src: ['worker', 'bar'], | ||
| style_src: ['style', 'dolly'], | ||
| }); | ||
| expect(config.rules).toEqual([ | ||
| `script-src script foo`, | ||
| `worker-src worker bar`, | ||
| `style-src style dolly`, | ||
| ]); | ||
| expect(config.header).toEqual( | ||
| `script-src script foo; worker-src worker bar; style-src style dolly` | ||
| ); | ||
| }); | ||
|
|
||
| test('applies defaults when `rules` is undefined', () => { | ||
| const config = new CspConfig({ | ||
| ...defaultConfig, | ||
| rules: undefined, | ||
| script_src: ['script'], | ||
| worker_src: ['worker'], | ||
| style_src: ['style'], | ||
| }); | ||
| expect(config.rules).toEqual([ | ||
| `script-src 'unsafe-eval' 'self' script`, | ||
| `worker-src blob: 'self' worker`, | ||
| `style-src 'unsafe-inline' 'self' style`, | ||
| ]); | ||
| expect(config.header).toEqual( | ||
| `script-src 'unsafe-eval' 'self' script; worker-src blob: 'self' worker; style-src 'unsafe-inline' 'self' style` | ||
| ); | ||
| }); | ||
|
|
||
| describe('allows "disableEmbedding" to be set', () => { | ||
| const disableEmbedding = true; | ||
|
|
||
| test('and changes rules/header if custom rules are not defined', () => { | ||
| const config = new CspConfig({ disableEmbedding }); | ||
| const config = new CspConfig({ ...defaultConfig, disableEmbedding }); | ||
| expect(config.disableEmbedding).toEqual(disableEmbedding); | ||
| expect(config.disableEmbedding).not.toEqual(CspConfig.DEFAULT.disableEmbedding); | ||
| expect(config.rules).toEqual(expect.arrayContaining([FRAME_ANCESTORS_RULE])); | ||
| expect(config.rules).toEqual(expect.arrayContaining([`frame-ancestors 'self'`])); | ||
| expect(config.header).toMatchInlineSnapshot( | ||
| `"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'; frame-ancestors 'self'"` | ||
| ); | ||
| }); | ||
|
|
||
| test('and does not change rules/header if custom rules are defined', () => { | ||
| const rules = ['foo', 'bar']; | ||
| const config = new CspConfig({ disableEmbedding, rules }); | ||
| const rules = [`foo 'self'`, `bar 'self'`]; | ||
| const config = new CspConfig({ ...defaultConfig, disableEmbedding, rules }); | ||
| expect(config.disableEmbedding).toEqual(disableEmbedding); | ||
| expect(config.disableEmbedding).not.toEqual(CspConfig.DEFAULT.disableEmbedding); | ||
| expect(config.rules).toEqual(rules); | ||
| expect(config.header).toMatchInlineSnapshot(`"foo; bar"`); | ||
| expect(config.header).toMatchInlineSnapshot(`"foo 'self'; bar 'self'"`); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,8 @@ | |
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| import { config, FRAME_ANCESTORS_RULE } from './config'; | ||
| import { config, CspConfigType } from './config'; | ||
| import { CspDirectives } from './csp_directives'; | ||
|
|
||
| const DEFAULT_CONFIG = Object.freeze(config.schema.validate({})); | ||
|
|
||
|
|
@@ -50,8 +51,9 @@ export interface ICspConfig { | |
| * @public | ||
| */ | ||
| export class CspConfig implements ICspConfig { | ||
| static readonly DEFAULT = new CspConfig(); | ||
| static readonly DEFAULT = new CspConfig(DEFAULT_CONFIG); | ||
|
|
||
| readonly #directives: CspDirectives; | ||
| public readonly rules: string[]; | ||
| public readonly strict: boolean; | ||
| public readonly warnLegacyBrowsers: boolean; | ||
|
|
@@ -62,16 +64,17 @@ export class CspConfig implements ICspConfig { | |
| * Returns the default CSP configuration when passed with no config | ||
| * @internal | ||
| */ | ||
| constructor(rawCspConfig: Partial<Omit<ICspConfig, 'header'>> = {}) { | ||
| const source = { ...DEFAULT_CONFIG, ...rawCspConfig }; | ||
|
|
||
| this.rules = [...source.rules]; | ||
| this.strict = source.strict; | ||
| this.warnLegacyBrowsers = source.warnLegacyBrowsers; | ||
| this.disableEmbedding = source.disableEmbedding; | ||
| if (!rawCspConfig.rules?.length && source.disableEmbedding) { | ||
| this.rules.push(FRAME_ANCESTORS_RULE); | ||
| constructor(rawCspConfig: CspConfigType) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The partial constructor thingy was just a workaround to instantiate the |
||
| this.#directives = CspDirectives.fromConfig(rawCspConfig); | ||
| if (!rawCspConfig.rules?.length && rawCspConfig.disableEmbedding) { | ||
| this.#directives.addDirectiveValue('frame-ancestors', `'self'`); | ||
|
pgayvallet marked this conversation as resolved.
|
||
| } | ||
|
Comment on lines
+69
to
72
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that we're supporting additive config, I'm not sure the |
||
| this.header = this.rules.join('; '); | ||
|
|
||
| this.rules = this.#directives.getRules(); | ||
| this.header = this.#directives.getCspHeader(); | ||
|
|
||
| this.strict = rawCspConfig.strict; | ||
| this.warnLegacyBrowsers = rawCspConfig.warnLegacyBrowsers; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self: we can probably remove this warning feature now that we no longer support IE11 |
||
| this.disableEmbedding = rawCspConfig.disableEmbedding; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.