mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* add additive csp configuration * add unit tests for new class * fix types * adapt test utils * fix tests * more unit tests on config * generated doc * review comments * update ascii doc * update ascii doc links * automatically add single quotes for keywords * add missing csp directives * add more tests * add additional settings to asciidoc * add null-check * revert test config props * fix usage collection usage * some review comments * last review comments * add kibana-docker variables * try to fix doc reference * try to fix doc reference again * fix tests # Conflicts: # src/core/server/csp/config.ts # src/core/server/csp/csp_config.test.ts
This commit is contained in:
parent
d2721e5134
commit
b02ddafaf4
16 changed files with 1245 additions and 52 deletions
|
@ -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;
|
||||
```
|
|
@ -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) | | <code></code> | |
|
||||
| [DEFAULT](./kibana-plugin-core-server.cspconfig.default.md) | <code>static</code> | <code>CspConfig</code> | |
|
||||
| [disableEmbedding](./kibana-plugin-core-server.cspconfig.disableembedding.md) | | <code>boolean</code> | |
|
||||
| [header](./kibana-plugin-core-server.cspconfig.header.md) | | <code>string</code> | |
|
||||
|
|
|
@ -36,11 +36,57 @@ Set to `false` to disable Console. *Default: `true`*
|
|||
<<ops-cGroupOverrides-cpuAcctPath, `ops.cGroupOverrides.cpuAcctPath`>>.
|
||||
|
||||
| `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
|
||||
<<server-securityResponseHeaders-disableEmbedding, `server.securityResponseHeaders.disableEmbedding`>>. 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
|
||||
|
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,28 +7,150 @@
|
|||
*/
|
||||
|
||||
import { TypeOf, schema } from '@kbn/config-schema';
|
||||
import { ServiceConfigDescriptor } from '../internal_types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type CspConfigType = TypeOf<typeof config.schema>;
|
||||
interface DirectiveValidationOptions {
|
||||
allowNone: boolean;
|
||||
allowNonce: boolean;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// 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'`,
|
||||
],
|
||||
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<boolean>(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
|
||||
);
|
||||
};
|
||||
|
||||
export const FRAME_ANCESTORS_RULE = `frame-ancestors 'self'`; // only used by CspConfig when embedding is disabled
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type CspConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: ServiceConfigDescriptor<CspConfigType> = {
|
||||
// TODO: Move this to server.csp using config deprecations
|
||||
// ? https://github.com/elastic/kibana/pull/52251
|
||||
path: 'csp',
|
||||
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".`,
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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'"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
266
src/core/server/csp/csp_directives.test.ts
Normal file
266
src/core/server/csp/csp_directives.test.ts
Normal file
|
@ -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'"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
159
src/core/server/csp/csp_directives.ts
Normal file
159
src/core/server/csp/csp_directives.ts
Normal file
|
@ -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<Record<CspDirectiveName, string[]>> = {
|
||||
'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<Record<CspDirectiveName, string[]>> = {
|
||||
'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<CspDirectiveName, Set<string>>();
|
||||
|
||||
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<Record<CspDirectiveName, string[]>> => {
|
||||
const directives: Partial<Record<CspDirectiveName, string[]>> = {};
|
||||
rules.forEach((rule) => {
|
||||
const [name, ...values] = rule.replace(/\s+/g, ' ').trim().split(' ');
|
||||
directives[name as CspDirectiveName] = values;
|
||||
});
|
||||
return directives;
|
||||
};
|
||||
|
||||
const parseConfigDirectives = (cspConfig: CspConfigType): Map<CspDirectiveName, string[]> => {
|
||||
const map = new Map<CspDirectiveName, string[]>();
|
||||
|
||||
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;
|
||||
}
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
|
|
|
@ -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<Omit<ICspConfig, 'header'>>);
|
||||
constructor(rawCspConfig: CspConfigType);
|
||||
// (undocumented)
|
||||
static readonly DEFAULT: CspConfig;
|
||||
// (undocumented)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,7 +22,21 @@ describe('csp collector', () => {
|
|||
const mockedFetchContext = createCollectorFetchContextMock();
|
||||
|
||||
function updateCsp(config: Partial<ICspConfig>) {
|
||||
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(() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue