diff --git a/docs/development/core/server/kibana-plugin-core-server.cspconfig.__private_.md b/docs/development/core/server/kibana-plugin-core-server.cspconfig.__private_.md
new file mode 100644
index 0000000000000..217066481d33c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.cspconfig.__private_.md
@@ -0,0 +1,11 @@
+
+
+[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
+
+Signature:
+
+```typescript
+#private;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.cspconfig.md b/docs/development/core/server/kibana-plugin-core-server.cspconfig.md
index 9f4f3211ea2b1..0337a1f4d3301 100644
--- a/docs/development/core/server/kibana-plugin-core-server.cspconfig.md
+++ b/docs/development/core/server/kibana-plugin-core-server.cspconfig.md
@@ -20,6 +20,7 @@ The constructor for this class is marked as internal. Third-party code should no
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
+| ["\#private"](./kibana-plugin-core-server.cspconfig.__private_.md) | |
| |
| [DEFAULT](./kibana-plugin-core-server.cspconfig.default.md) | static
| CspConfig
| |
| [disableEmbedding](./kibana-plugin-core-server.cspconfig.disableembedding.md) | | boolean
| |
| [header](./kibana-plugin-core-server.cspconfig.header.md) | | string
| |
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index c3c29adcea18f..bcaa86d73adc4 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -36,11 +36,57 @@ Set to `false` to disable Console. *Default: `true`*
<>.
| `csp.rules:`
- | A https://w3c.github.io/webappsec-csp/[content-security-policy] template
+ | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."]
+A https://w3c.github.io/webappsec-csp/[Content Security Policy] template
that disables certain unnecessary and potentially insecure capabilities in
the browser. It is strongly recommended that you keep the default CSP rules
that ship with {kib}.
+| `csp.script_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src[Content Security Policy `script-src` directive].
+
+| `csp.worker_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src[Content Security Policy `worker-src` directive].
+
+| `csp.style_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src[Content Security Policy `style-src` directive].
+
+| `csp.connect_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src[Content Security Policy `connect-src` directive].
+
+| `csp.default_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src[Content Security Policy `default-src` directive].
+
+| `csp.font_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/font-src[Content Security Policy `font-src` directive].
+
+| `csp.frame_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src[Content Security Policy `frame-src` directive].
+
+| `csp.img_src:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src[Content Security Policy `img-src` directive].
+
+| `csp.frame_ancestors:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors[Content Security Policy `frame-ancestors` directive].
+
+|===
+
+[NOTE]
+============
+The `frame-ancestors` directive can also be configured by using
+<>. In that case, that takes precedence and any values in `csp.frame_ancestors`
+are ignored.
+============
+
+[cols="2*<"]
+|===
+
+| `csp.report_uri:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri[Content Security Policy `report-uri` directive].
+
+| `csp.report_to:`
+| Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to[Content Security Policy `report-to` directive].
+
|[[csp-strict]] `csp.strict:`
| Blocks {kib} access to any browser that
does not enforce even rudimentary CSP rules. In practice, this disables
@@ -538,8 +584,7 @@ a|`server.securityResponseHeaders:`
is used in all responses to the client from the {kib} server, and specifies what value is used. Allowed values are any text value or `null`.
To disable, set to `null`. *Default:* `null`
-[[server-securityResponseHeaders-disableEmbedding]]
-a|`server.securityResponseHeaders:`
+|[[server-securityResponseHeaders-disableEmbedding]]`server.securityResponseHeaders:`
`disableEmbedding:`
| Controls whether the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy[`Content-Security-Policy`] and
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options[`X-Frame-Options`] headers are configured to disable embedding
diff --git a/src/core/server/csp/config.test.ts b/src/core/server/csp/config.test.ts
index 8036ebeeaad31..6db93addb7da8 100644
--- a/src/core/server/csp/config.test.ts
+++ b/src/core/server/csp/config.test.ts
@@ -9,11 +9,469 @@
import { config } from './config';
describe('config.validate()', () => {
- test(`does not allow "disableEmbedding" to be set to true`, () => {
+ it(`does not allow "disableEmbedding" to be set to true`, () => {
// This is intentionally not editable in the raw CSP config.
// Users should set `server.securityResponseHeaders.disableEmbedding` to control this config property.
expect(() => config.schema.validate({ disableEmbedding: true })).toThrowError(
'[disableEmbedding]: expected value to equal [false]'
);
});
+
+ describe(`"script_src"`, () => {
+ it(`throws if containing 'unsafe-inline' when 'strict' is true`, () => {
+ expect(() =>
+ config.schema.validate({
+ strict: true,
+ warnLegacyBrowsers: false,
+ script_src: [`'self'`, `unsafe-inline`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"cannot use \`unsafe-inline\` for \`script_src\` when \`csp.strict\` is true"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ strict: true,
+ warnLegacyBrowsers: false,
+ script_src: [`'self'`, `'unsafe-inline'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"cannot use \`unsafe-inline\` for \`script_src\` when \`csp.strict\` is true"`
+ );
+ });
+
+ it(`throws if containing 'unsafe-inline' when 'warnLegacyBrowsers' is true`, () => {
+ expect(() =>
+ config.schema.validate({
+ strict: false,
+ warnLegacyBrowsers: true,
+ script_src: [`'self'`, `unsafe-inline`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"cannot use \`unsafe-inline\` for \`script_src\` when \`csp.warnLegacyBrowsers\` is true"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ strict: false,
+ warnLegacyBrowsers: true,
+ script_src: [`'self'`, `'unsafe-inline'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"cannot use \`unsafe-inline\` for \`script_src\` when \`csp.warnLegacyBrowsers\` is true"`
+ );
+ });
+
+ it(`does not throw if containing 'unsafe-inline' when 'strict' and 'warnLegacyBrowsers' are false`, () => {
+ expect(() =>
+ config.schema.validate({
+ strict: false,
+ warnLegacyBrowsers: false,
+ script_src: [`'self'`, `unsafe-inline`],
+ })
+ ).not.toThrow();
+
+ expect(() =>
+ config.schema.validate({
+ strict: false,
+ warnLegacyBrowsers: false,
+ script_src: [`'self'`, `'unsafe-inline'`],
+ })
+ ).not.toThrow();
+ });
+
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ script_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ script_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[script_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ script_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[script_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ script_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[script_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"worker_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ worker_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ worker_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[worker_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ worker_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[worker_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ worker_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[worker_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"style_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ style_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ style_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[style_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ style_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[style_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ style_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[style_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"connect_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ connect_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ connect_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[connect_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ connect_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[connect_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ connect_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[connect_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"default_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ default_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ default_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[default_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ default_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[default_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ default_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[default_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"font_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ font_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ font_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[font_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ font_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[font_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ font_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[font_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"frame_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ frame_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ frame_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[frame_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ frame_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[frame_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ frame_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[frame_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"img_src"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ img_src: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ img_src: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[img_src]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ img_src: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[img_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ img_src: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[img_src]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
+
+ describe(`"frame_ancestors"`, () => {
+ it(`throws if 'rules' is also specified`, () => {
+ expect(() =>
+ config.schema.validate({
+ rules: [
+ `script-src 'unsafe-eval' 'self'`,
+ `worker-src 'unsafe-eval' 'self'`,
+ `style-src 'unsafe-eval' 'self'`,
+ ],
+ frame_ancestors: [`'self'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""`
+ );
+ });
+
+ it('throws if using an `nonce-*` value', () => {
+ expect(() =>
+ config.schema.validate({
+ frame_ancestors: [`hello`, `nonce-foo`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[frame_ancestors]: using \\"nonce-*\\" is considered insecure and is not allowed"`
+ );
+ });
+ it("throws if using `none` or `'none'`", () => {
+ expect(() =>
+ config.schema.validate({
+ frame_ancestors: [`hello`, `none`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[frame_ancestors]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+
+ expect(() =>
+ config.schema.validate({
+ frame_ancestors: [`hello`, `'none'`],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"[frame_ancestors]: using \\"none\\" would conflict with Kibana's default csp configuration and is not allowed"`
+ );
+ });
+ });
});
diff --git a/src/core/server/csp/config.ts b/src/core/server/csp/config.ts
index 9f8a61bf8e0f4..16a2fa4e62894 100644
--- a/src/core/server/csp/config.ts
+++ b/src/core/server/csp/config.ts
@@ -7,28 +7,150 @@
*/
import { TypeOf, schema } from '@kbn/config-schema';
+import { ServiceConfigDescriptor } from '../internal_types';
+
+interface DirectiveValidationOptions {
+ allowNone: boolean;
+ allowNonce: boolean;
+}
+
+const getDirectiveValidator = (options: DirectiveValidationOptions) => {
+ const validateValue = getDirectiveValueValidator(options);
+ return (values: string[]) => {
+ for (const value of values) {
+ const error = validateValue(value);
+ if (error) {
+ return error;
+ }
+ }
+ };
+};
+
+const getDirectiveValueValidator = ({ allowNone, allowNonce }: DirectiveValidationOptions) => {
+ return (value: string) => {
+ if (!allowNonce && value.startsWith('nonce-')) {
+ return `using "nonce-*" is considered insecure and is not allowed`;
+ }
+ if (!allowNone && (value === `none` || value === `'none'`)) {
+ return `using "none" would conflict with Kibana's default csp configuration and is not allowed`;
+ }
+ };
+};
+
+const configSchema = schema.object(
+ {
+ rules: schema.maybe(schema.arrayOf(schema.string())),
+ script_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ worker_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ style_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ connect_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ default_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ font_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ frame_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ img_src: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ frame_ancestors: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: false, allowNonce: false }),
+ }),
+ report_uri: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ validate: getDirectiveValidator({ allowNone: true, allowNonce: false }),
+ }),
+ report_to: schema.arrayOf(schema.string(), {
+ defaultValue: [],
+ }),
+ strict: schema.boolean({ defaultValue: false }),
+ warnLegacyBrowsers: schema.boolean({ defaultValue: true }),
+ disableEmbedding: schema.oneOf([schema.literal(false)], { defaultValue: false }),
+ },
+ {
+ validate: (cspConfig) => {
+ if (cspConfig.rules && hasDirectiveSpecified(cspConfig)) {
+ return `"csp.rules" cannot be used when specifying per-directive additions such as "script_src", "worker_src" or "style_src"`;
+ }
+ const hasUnsafeInlineScriptSrc =
+ cspConfig.script_src.includes(`unsafe-inline`) ||
+ cspConfig.script_src.includes(`'unsafe-inline'`);
+
+ if (cspConfig.strict && hasUnsafeInlineScriptSrc) {
+ return 'cannot use `unsafe-inline` for `script_src` when `csp.strict` is true';
+ }
+ if (cspConfig.warnLegacyBrowsers && hasUnsafeInlineScriptSrc) {
+ return 'cannot use `unsafe-inline` for `script_src` when `csp.warnLegacyBrowsers` is true';
+ }
+ },
+ }
+);
+
+const hasDirectiveSpecified = (rawConfig: CspConfigType): boolean => {
+ return Boolean(
+ rawConfig.script_src.length ||
+ rawConfig.worker_src.length ||
+ rawConfig.style_src.length ||
+ rawConfig.connect_src.length ||
+ rawConfig.default_src.length ||
+ rawConfig.font_src.length ||
+ rawConfig.frame_src.length ||
+ rawConfig.img_src.length ||
+ rawConfig.frame_ancestors.length ||
+ rawConfig.report_uri.length ||
+ rawConfig.report_to.length
+ );
+};
/**
* @internal
*/
-export type CspConfigType = TypeOf;
+export type CspConfigType = TypeOf;
-export const config = {
+export const config: ServiceConfigDescriptor = {
// 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: false }),
- warnLegacyBrowsers: schema.boolean({ defaultValue: true }),
- disableEmbedding: schema.oneOf([schema.literal(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.connect_src`, ' +
+ '`csp.default_src`, `csp.font_src`, `csp.frame_ancestors`, `csp.frame_src`, `csp.img_src`, ' +
+ '`csp.report_uri`, `csp.report_to`, `csp.script_src`, `csp.style_src`, and `csp.worker_src` instead.',
+ correctiveActions: {
+ manualSteps: [
+ `Remove "csp.rules" from the Kibana config file."`,
+ `Add directive specific configurations to the config file using "csp.connect_src", "csp.default_src", "csp.font_src", ` +
+ `"csp.frame_ancestors", "csp.frame_src", "csp.img_src", "csp.report_uri", "csp.report_to", "csp.script_src", ` +
+ `"csp.style_src", and "csp.worker_src".`,
+ ],
+ },
+ });
+ }
+ },
+ ],
};
-
-export const FRAME_ANCESTORS_RULE = `frame-ancestors 'self'`; // only used by CspConfig when embedding is disabled
diff --git a/src/core/server/csp/csp_config.test.ts b/src/core/server/csp/csp_config.test.ts
index 373a47b55314d..43dfb12957fe8 100644
--- a/src/core/server/csp/csp_config.test.ts
+++ b/src/core/server/csp/csp_config.test.ts
@@ -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,129 @@ 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: true });
+ const config = new CspConfig({ ...defaultConfig, strict: true });
expect(config.strict).toEqual(true);
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'],
+ });
+ 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'"`);
+ });
+
+ test('and overrides `frame-ancestors` if set', () => {
+ const config = new CspConfig({
+ ...defaultConfig,
+ disableEmbedding: true,
+ frame_ancestors: ['foo.com'],
+ });
+ expect(config.disableEmbedding).toEqual(disableEmbedding);
+ expect(config.disableEmbedding).not.toEqual(CspConfig.DEFAULT.disableEmbedding);
+ expect(config.header).toMatchInlineSnapshot(
+ `"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'; frame-ancestors 'self'"`
+ );
});
});
});
diff --git a/src/core/server/csp/csp_config.ts b/src/core/server/csp/csp_config.ts
index 649c81576ef52..13778088d9df2 100644
--- a/src/core/server/csp/csp_config.ts
+++ b/src/core/server/csp/csp_config.ts
@@ -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,18 @@ export class CspConfig implements ICspConfig {
* 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.disableEmbedding = source.disableEmbedding;
- if (!rawCspConfig.rules?.length && source.disableEmbedding) {
- this.rules.push(FRAME_ANCESTORS_RULE);
+ constructor(rawCspConfig: CspConfigType) {
+ this.#directives = CspDirectives.fromConfig(rawCspConfig);
+ if (!rawCspConfig.rules?.length && rawCspConfig.disableEmbedding) {
+ this.#directives.clearDirectiveValues('frame-ancestors');
+ this.#directives.addDirectiveValue('frame-ancestors', `'self'`);
}
- this.header = this.rules.join('; ');
+
+ this.rules = this.#directives.getRules();
+ this.header = this.#directives.getCspHeader();
+
+ this.strict = rawCspConfig.strict;
+ this.warnLegacyBrowsers = rawCspConfig.warnLegacyBrowsers;
+ this.disableEmbedding = rawCspConfig.disableEmbedding;
}
}
diff --git a/src/core/server/csp/csp_directives.test.ts b/src/core/server/csp/csp_directives.test.ts
new file mode 100644
index 0000000000000..1077b6ea9f3cd
--- /dev/null
+++ b/src/core/server/csp/csp_directives.test.ts
@@ -0,0 +1,266 @@
+/*
+ * 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', 'dolly');
+
+ expect(directives.getCspHeader()).toMatchInlineSnapshot(
+ `"style-src foo bar; worker-src dolly"`
+ );
+ expect(directives.getRules()).toMatchInlineSnapshot(`
+ Array [
+ "style-src foo bar",
+ "worker-src dolly",
+ ]
+ `);
+ });
+
+ 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",
+ ]
+ `);
+ });
+
+ it('automatically adds single quotes for keywords', () => {
+ const directives = new CspDirectives();
+ directives.addDirectiveValue('style-src', 'none');
+ directives.addDirectiveValue('style-src', 'self');
+ directives.addDirectiveValue('style-src', 'strict-dynamic');
+ directives.addDirectiveValue('style-src', 'report-sample');
+ directives.addDirectiveValue('style-src', 'unsafe-inline');
+ directives.addDirectiveValue('style-src', 'unsafe-eval');
+ directives.addDirectiveValue('style-src', 'unsafe-hashes');
+ directives.addDirectiveValue('style-src', 'unsafe-allow-redirects');
+
+ expect(directives.getCspHeader()).toMatchInlineSnapshot(
+ `"style-src 'none' 'self' 'strict-dynamic' 'report-sample' 'unsafe-inline' 'unsafe-eval' 'unsafe-hashes' 'unsafe-allow-redirects'"`
+ );
+ });
+
+ it('does not add single quotes for keywords when already present', () => {
+ const directives = new CspDirectives();
+ directives.addDirectiveValue('style-src', `'none'`);
+ directives.addDirectiveValue('style-src', `'self'`);
+ directives.addDirectiveValue('style-src', `'strict-dynamic'`);
+ directives.addDirectiveValue('style-src', `'report-sample'`);
+ directives.addDirectiveValue('style-src', `'unsafe-inline'`);
+ directives.addDirectiveValue('style-src', `'unsafe-eval'`);
+ directives.addDirectiveValue('style-src', `'unsafe-hashes'`);
+ directives.addDirectiveValue('style-src', `'unsafe-allow-redirects'`);
+
+ expect(directives.getCspHeader()).toMatchInlineSnapshot(
+ `"style-src 'none' 'self' 'strict-dynamic' 'report-sample' 'unsafe-inline' 'unsafe-eval' 'unsafe-hashes' 'unsafe-allow-redirects'"`
+ );
+ });
+ });
+
+ 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 single quotes for keyword for 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('handles multiple whitespaces when parsing 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('supports unregistered directives', () => {
+ const config = cspConfig.schema.validate({
+ rules: [`script-src 'self' http://foo.com`, `img-src 'self'`, 'foo bar'],
+ });
+ const directives = CspDirectives.fromConfig(config);
+
+ expect(directives.getRules()).toMatchInlineSnapshot(`
+ Array [
+ "script-src 'self' http://foo.com",
+ "img-src 'self'",
+ "foo bar",
+ ]
+ `);
+ expect(directives.getCspHeader()).toMatchInlineSnapshot(
+ `"script-src 'self' http://foo.com; img-src 'self'; foo bar"`
+ );
+ });
+
+ it('adds default value for config with directives', () => {
+ 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"`
+ );
+ });
+
+ it('adds additional values for some directives without defaults', () => {
+ const config = cspConfig.schema.validate({
+ connect_src: [`connect-src`],
+ default_src: [`default-src`],
+ font_src: [`font-src`],
+ frame_src: [`frame-src`],
+ img_src: [`img-src`],
+ frame_ancestors: [`frame-ancestors`],
+ report_uri: [`report-uri`],
+ report_to: [`report-to`],
+ });
+ const directives = CspDirectives.fromConfig(config);
+
+ expect(directives.getRules()).toMatchInlineSnapshot(`
+ Array [
+ "script-src 'unsafe-eval' 'self'",
+ "worker-src blob: 'self'",
+ "style-src 'unsafe-inline' 'self'",
+ "connect-src 'self' connect-src",
+ "default-src 'self' default-src",
+ "font-src 'self' font-src",
+ "frame-src 'self' frame-src",
+ "img-src 'self' img-src",
+ "frame-ancestors 'self' frame-ancestors",
+ "report-uri report-uri",
+ "report-to report-to",
+ ]
+ `);
+ });
+
+ it('adds single quotes for keywords in added directives', () => {
+ const config = cspConfig.schema.validate({
+ script_src: [`unsafe-hashes`],
+ });
+ const directives = CspDirectives.fromConfig(config);
+
+ expect(directives.getRules()).toMatchInlineSnapshot(`
+ Array [
+ "script-src 'unsafe-eval' 'self' 'unsafe-hashes'",
+ "worker-src blob: 'self'",
+ "style-src 'unsafe-inline' 'self'",
+ ]
+ `);
+ expect(directives.getCspHeader()).toMatchInlineSnapshot(
+ `"script-src 'unsafe-eval' 'self' 'unsafe-hashes'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'"`
+ );
+ });
+ });
+});
diff --git a/src/core/server/csp/csp_directives.ts b/src/core/server/csp/csp_directives.ts
new file mode 100644
index 0000000000000..9e3b60f7f1e4f
--- /dev/null
+++ b/src/core/server/csp/csp_directives.ts
@@ -0,0 +1,159 @@
+/*
+ * 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 { CspConfigType } from './config';
+
+export type CspDirectiveName =
+ | 'script-src'
+ | 'worker-src'
+ | 'style-src'
+ | 'frame-ancestors'
+ | 'connect-src'
+ | 'default-src'
+ | 'font-src'
+ | 'frame-src'
+ | 'img-src'
+ | 'report-uri'
+ | 'report-to';
+
+/**
+ * The default rules that are always applied
+ */
+export const defaultRules: Partial> = {
+ 'script-src': [`'unsafe-eval'`, `'self'`],
+ 'worker-src': [`blob:`, `'self'`],
+ 'style-src': [`'unsafe-inline'`, `'self'`],
+};
+
+/**
+ * Per-directive rules that will be added when the configuration contains at least one value
+ * Main purpose is to add `self` value to some directives when the configuration specifies other values
+ */
+export const additionalRules: Partial> = {
+ 'connect-src': [`'self'`],
+ 'default-src': [`'self'`],
+ 'font-src': [`'self'`],
+ 'img-src': [`'self'`],
+ 'frame-ancestors': [`'self'`],
+ 'frame-src': [`'self'`],
+};
+
+export class CspDirectives {
+ private readonly directives = new Map>();
+
+ addDirectiveValue(directiveName: CspDirectiveName, directiveValue: string) {
+ if (!this.directives.has(directiveName)) {
+ this.directives.set(directiveName, new Set());
+ }
+ this.directives.get(directiveName)!.add(normalizeDirectiveValue(directiveValue));
+ }
+
+ clearDirectiveValues(directiveName: CspDirectiveName) {
+ this.directives.delete(directiveName);
+ }
+
+ getCspHeader() {
+ return this.getRules().join('; ');
+ }
+
+ getRules() {
+ return [...this.directives.entries()].map(([name, values]) => {
+ return [name, ...values].join(' ');
+ });
+ }
+
+ static fromConfig(config: CspConfigType): CspDirectives {
+ const cspDirectives = new CspDirectives();
+
+ // adding `csp.rules` or `default` rules
+ const initialRules = config.rules ? parseRules(config.rules) : { ...defaultRules };
+ Object.entries(initialRules).forEach(([key, values]) => {
+ values?.forEach((value) => {
+ cspDirectives.addDirectiveValue(key as CspDirectiveName, value);
+ });
+ });
+
+ // adding per-directive configuration
+ const additiveConfig = parseConfigDirectives(config);
+ [...additiveConfig.entries()].forEach(([directiveName, directiveValues]) => {
+ const additionalValues = additionalRules[directiveName] ?? [];
+ [...additionalValues, ...directiveValues].forEach((value) => {
+ cspDirectives.addDirectiveValue(directiveName, value);
+ });
+ });
+
+ return cspDirectives;
+ }
+}
+
+const parseRules = (rules: string[]): Partial> => {
+ const directives: Partial> = {};
+ rules.forEach((rule) => {
+ const [name, ...values] = rule.replace(/\s+/g, ' ').trim().split(' ');
+ directives[name as CspDirectiveName] = values;
+ });
+ return directives;
+};
+
+const parseConfigDirectives = (cspConfig: CspConfigType): Map => {
+ const map = new Map();
+
+ if (cspConfig.script_src?.length) {
+ map.set('script-src', cspConfig.script_src);
+ }
+ if (cspConfig.worker_src?.length) {
+ map.set('worker-src', cspConfig.worker_src);
+ }
+ if (cspConfig.style_src?.length) {
+ map.set('style-src', cspConfig.style_src);
+ }
+ if (cspConfig.connect_src?.length) {
+ map.set('connect-src', cspConfig.connect_src);
+ }
+ if (cspConfig.default_src?.length) {
+ map.set('default-src', cspConfig.default_src);
+ }
+ if (cspConfig.font_src?.length) {
+ map.set('font-src', cspConfig.font_src);
+ }
+ if (cspConfig.frame_src?.length) {
+ map.set('frame-src', cspConfig.frame_src);
+ }
+ if (cspConfig.img_src?.length) {
+ map.set('img-src', cspConfig.img_src);
+ }
+ if (cspConfig.frame_ancestors?.length) {
+ map.set('frame-ancestors', cspConfig.frame_ancestors);
+ }
+ if (cspConfig.report_uri?.length) {
+ map.set('report-uri', cspConfig.report_uri);
+ }
+ if (cspConfig.report_to?.length) {
+ map.set('report-to', cspConfig.report_to);
+ }
+
+ return map;
+};
+
+const keywordTokens = [
+ 'none',
+ 'self',
+ 'strict-dynamic',
+ 'report-sample',
+ 'unsafe-inline',
+ 'unsafe-eval',
+ 'unsafe-hashes',
+ 'unsafe-allow-redirects',
+];
+
+function normalizeDirectiveValue(value: string) {
+ if (keywordTokens.includes(value)) {
+ return `'${value}'`;
+ }
+ return value;
+}
diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts
index c802163866423..55af02a08561b 100644
--- a/src/core/server/http/cookie_session_storage.test.ts
+++ b/src/core/server/http/cookie_session_storage.test.ts
@@ -69,7 +69,11 @@ configService.atPath.mockImplementation((path) => {
} as any);
}
if (path === 'csp') {
- return new BehaviorSubject({} as any);
+ return new BehaviorSubject({
+ strict: false,
+ disableEmbedding: false,
+ warnLegacyBrowsers: true,
+ });
}
throw new Error(`Unexpected config path: ${path}`);
});
diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts
index 20efa6d125cfb..e7cfe2cf89e89 100644
--- a/src/core/server/http/http_config.test.ts
+++ b/src/core/server/http/http_config.test.ts
@@ -8,7 +8,7 @@
import uuid from 'uuid';
import { config, HttpConfig } from './http_config';
-import { CspConfig } from '../csp';
+import { config as cspConfig } from '../csp';
import { ExternalUrlConfig } from '../external_url';
const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost'];
@@ -465,7 +465,8 @@ describe('HttpConfig', () => {
},
},
});
- const httpConfig = new HttpConfig(rawConfig, CspConfig.DEFAULT, ExternalUrlConfig.DEFAULT);
+ const rawCspConfig = cspConfig.schema.validate({});
+ const httpConfig = new HttpConfig(rawConfig, rawCspConfig, ExternalUrlConfig.DEFAULT);
expect(httpConfig.customResponseHeaders).toEqual({
string: 'string',
diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
index cbd300fdc9c09..c2023c5577d61 100644
--- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
@@ -79,7 +79,11 @@ describe('core lifecycle handlers', () => {
} as any);
}
if (path === 'csp') {
- return new BehaviorSubject({} as any);
+ return new BehaviorSubject({
+ strict: false,
+ disableEmbedding: false,
+ warnLegacyBrowsers: true,
+ });
}
throw new Error(`Unexpected config path: ${path}`);
});
diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts
index b3180b43d0026..4e1a88e967f8f 100644
--- a/src/core/server/http/test_utils.ts
+++ b/src/core/server/http/test_utils.ts
@@ -56,7 +56,11 @@ configService.atPath.mockImplementation((path) => {
} as any);
}
if (path === 'csp') {
- return new BehaviorSubject({} as any);
+ return new BehaviorSubject({
+ strict: false,
+ disableEmbedding: false,
+ warnLegacyBrowsers: true,
+ });
}
throw new Error(`Unexpected config path: ${path}`);
});
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index fcecf39f7e53a..3bc0b54635eb5 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -777,8 +777,12 @@ export interface CountResponse {
// @public
export class CspConfig implements ICspConfig {
+ // (undocumented)
+ #private;
+ // Warning: (ae-forgotten-export) The symbol "CspConfigType" needs to be exported by the entry point index.d.ts
+ //
// @internal
- constructor(rawCspConfig?: Partial>);
+ constructor(rawCspConfig: CspConfigType);
// (undocumented)
static readonly DEFAULT: CspConfig;
// (undocumented)
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index 6fd5de33c5f5c..034868c59f275 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -31,6 +31,17 @@ kibana_vars=(
csp.rules
csp.strict
csp.warnLegacyBrowsers
+ csp.script_src
+ csp.worker_src
+ csp.style_src
+ csp.connect_src
+ csp.default_src
+ csp.font_src
+ csp.frame_src
+ csp.img_src
+ csp.frame_ancestors
+ csp.report_uri
+ csp.report_to
data.autocomplete.valueSuggestions.terminateAfter
data.autocomplete.valueSuggestions.timeout
elasticsearch.customHeaders
diff --git a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts
index 34faf345cfed3..8a0f6010cbe4c 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts
@@ -22,7 +22,21 @@ describe('csp collector', () => {
const mockedFetchContext = createCollectorFetchContextMock();
function updateCsp(config: Partial) {
- httpMock.csp = new CspConfig(config);
+ httpMock.csp = new CspConfig({
+ ...CspConfig.DEFAULT,
+ style_src: [],
+ worker_src: [],
+ script_src: [],
+ connect_src: [],
+ default_src: [],
+ font_src: [],
+ frame_src: [],
+ img_src: [],
+ frame_ancestors: [],
+ report_uri: [],
+ report_to: [],
+ ...config,
+ });
}
beforeEach(() => {