-
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 6 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 |
|---|---|---|
|
|
@@ -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: [] }), | ||
| 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 per-directive definitions. ' + | ||
|
pgayvallet marked this conversation as resolved.
Outdated
|
||
| '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 per-directive definitions to the config file, using "csp.script_src", "csp.worker_src" and "csp.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. Not sure it's the best wording, suggestions welcome
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 |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| import { CspDirectives } from './csp_directives'; | ||
| import { config as cspConfig } from './config'; | ||
|
|
||
| describe('CspDirectives', () => { | ||
| describe('#addDirectiveValue', () => { | ||
| it('properly updates the rules', () => { | ||
| const directives = new CspDirectives(); | ||
| directives.addDirectiveValue('style-src', 'foo'); | ||
|
|
||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "style-src foo", | ||
| ] | ||
| `); | ||
|
|
||
| directives.addDirectiveValue('style-src', 'bar'); | ||
|
|
||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "style-src foo bar", | ||
| ] | ||
| `); | ||
| }); | ||
|
|
||
| it('properly updates the header', () => { | ||
| const directives = new CspDirectives(); | ||
| directives.addDirectiveValue('style-src', 'foo'); | ||
|
|
||
| expect(directives.getCspHeader()).toMatchInlineSnapshot(`"style-src foo"`); | ||
|
|
||
| directives.addDirectiveValue('style-src', 'bar'); | ||
|
|
||
| expect(directives.getCspHeader()).toMatchInlineSnapshot(`"style-src foo bar"`); | ||
| }); | ||
|
|
||
| it('handles distinct directives', () => { | ||
| const directives = new CspDirectives(); | ||
| directives.addDirectiveValue('style-src', 'foo'); | ||
| directives.addDirectiveValue('style-src', 'bar'); | ||
| directives.addDirectiveValue('worker-src', 'self'); | ||
|
|
||
| expect(directives.getCspHeader()).toMatchInlineSnapshot( | ||
| `"style-src foo bar; worker-src self"` | ||
| ); | ||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "style-src foo bar", | ||
| "worker-src self", | ||
| ] | ||
| `); | ||
| }); | ||
|
|
||
| it('removes duplicates', () => { | ||
| const directives = new CspDirectives(); | ||
| directives.addDirectiveValue('style-src', 'foo'); | ||
| directives.addDirectiveValue('style-src', 'foo'); | ||
| directives.addDirectiveValue('style-src', 'bar'); | ||
|
|
||
| expect(directives.getCspHeader()).toMatchInlineSnapshot(`"style-src foo bar"`); | ||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "style-src foo bar", | ||
| ] | ||
| `); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#fromConfig', () => { | ||
| it('returns the correct rules for the default config', () => { | ||
| const config = cspConfig.schema.validate({}); | ||
| const directives = CspDirectives.fromConfig(config); | ||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "script-src 'unsafe-eval' 'self'", | ||
| "worker-src blob: 'self'", | ||
| "style-src 'unsafe-inline' 'self'", | ||
| ] | ||
| `); | ||
| }); | ||
|
|
||
| it('returns the correct header for the default config', () => { | ||
| const config = cspConfig.schema.validate({}); | ||
| const directives = CspDirectives.fromConfig(config); | ||
| expect(directives.getCspHeader()).toMatchInlineSnapshot( | ||
| `"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'"` | ||
| ); | ||
| }); | ||
|
|
||
| it('handles config with rules', () => { | ||
| const config = cspConfig.schema.validate({ | ||
| rules: [`script-src 'self' http://foo.com`, `worker-src 'self'`], | ||
| }); | ||
| const directives = CspDirectives.fromConfig(config); | ||
|
|
||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "script-src 'self' http://foo.com", | ||
| "worker-src 'self'", | ||
| ] | ||
| `); | ||
| expect(directives.getCspHeader()).toMatchInlineSnapshot( | ||
| `"script-src 'self' http://foo.com; worker-src 'self'"` | ||
| ); | ||
| }); | ||
|
|
||
| it('adds default value for config with directives', () => { | ||
|
pgayvallet marked this conversation as resolved.
|
||
| const config = cspConfig.schema.validate({ | ||
| script_src: [`baz`], | ||
| worker_src: [`foo`], | ||
| style_src: [`bar`, `dolly`], | ||
| }); | ||
| const directives = CspDirectives.fromConfig(config); | ||
|
|
||
| expect(directives.getRules()).toMatchInlineSnapshot(` | ||
| Array [ | ||
| "script-src 'unsafe-eval' 'self' baz", | ||
| "worker-src blob: 'self' foo", | ||
| "style-src 'unsafe-inline' 'self' bar dolly", | ||
| ] | ||
| `); | ||
| expect(directives.getCspHeader()).toMatchInlineSnapshot( | ||
| `"script-src 'unsafe-eval' 'self' baz; worker-src blob: 'self' foo; style-src 'unsafe-inline' 'self' bar dolly"` | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.