Skip to content

Commit 92ff168

Browse files
Content-Security-Policy-Report-Only (#10800)
* csp-report-only Signed-off-by: Justin Kim <[email protected]> * make some changes to test Signed-off-by: Justin Kim <[email protected]> * Changeset file for PR #10800 created/updated * make updates Signed-off-by: Justin Kim <[email protected]> * update with the final set of rules and extra config options Signed-off-by: Justin Kim <[email protected]> * remove form-action. was added due to local testing by accident Signed-off-by: Justin Kim <[email protected]> --------- Signed-off-by: Justin Kim <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 091d031 commit 92ff168

File tree

19 files changed

+563
-8
lines changed

19 files changed

+563
-8
lines changed

changelogs/fragments/10800.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Add a Content-Security-Policy-Report-Only header ([#10800](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10800))

src/core/server/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* Trusted endpoints that may be called for connect-src and img-src in our CSP directives.
8+
*/
9+
export const CSP_TRUSTED_ENDPOINTS = [
10+
'https://opensearch.org',
11+
'https://docs.opensearch.org',
12+
'https://maps.opensearch.org',
13+
'https://vectors.maps.opensearch.org',
14+
'https://tiles.maps.opensearch.org',
15+
];
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { TypeOf, schema } from '@osd/config-schema';
7+
import { CSP_TRUSTED_ENDPOINTS } from '../constants';
8+
9+
/**
10+
* @internal
11+
*/
12+
export type CspReportOnlyConfigType = TypeOf<typeof config.schema>;
13+
14+
export const config = {
15+
path: 'csp-report-only',
16+
schema: schema.object({
17+
isEmitting: schema.boolean({ defaultValue: false }),
18+
rules: schema.arrayOf(schema.string(), {
19+
defaultValue: [
20+
`default-src 'self'`,
21+
`script-src 'self'`,
22+
`script-src-attr 'none'`,
23+
`style-src 'self'`,
24+
`child-src 'none'`,
25+
`worker-src 'self'`,
26+
`frame-src 'none'`,
27+
`object-src 'none'`,
28+
`manifest-src 'self'`,
29+
`media-src 'none'`,
30+
`font-src 'self'`,
31+
`connect-src 'self' ${CSP_TRUSTED_ENDPOINTS.join(' ')}`,
32+
`img-src 'self' data: ${CSP_TRUSTED_ENDPOINTS.join(' ')}`,
33+
`form-action 'self'`,
34+
`frame-ancestors 'self'`,
35+
],
36+
}),
37+
endpoint: schema.maybe(schema.string()),
38+
useDeprecatedReportUriOnly: schema.boolean({ defaultValue: false }),
39+
allowedFrameAncestorSources: schema.maybe(schema.arrayOf(schema.string())),
40+
allowedConnectSources: schema.maybe(schema.arrayOf(schema.string())),
41+
allowedImgSources: schema.maybe(schema.arrayOf(schema.string())),
42+
}),
43+
};
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { CspReportOnlyConfig } from './csp_report_only_config';
7+
8+
// CSP-Report-Only rules aren't strictly additive, so any change can potentially expand or
9+
// restrict the policy in a way we consider a breaking change. For that reason,
10+
// we test the default rules exactly so any change to those rules gets flagged
11+
// for manual review. In other words, this test is intentionally fragile to draw
12+
// extra attention if defaults are modified in any way.
13+
//
14+
// A test failure here does not necessarily mean this change cannot be made,
15+
// but any change here should undergo sufficient scrutiny by the OpenSearch Dashboards
16+
// security team.
17+
//
18+
// The tests use inline snapshots to make it as easy as possible to identify
19+
// the nature of a change in defaults during a PR review.
20+
21+
describe('CspReportOnlyConfig', () => {
22+
describe('when no endpoint is configured', () => {
23+
test('DEFAULT', () => {
24+
expect(CspReportOnlyConfig.DEFAULT).toMatchInlineSnapshot(`
25+
CspReportOnlyConfig {
26+
"cspReportOnlyHeader": "default-src 'self'; script-src 'self'; script-src-attr 'none'; style-src 'self'; child-src 'none'; worker-src 'self'; frame-src 'none'; object-src 'none'; manifest-src 'self'; media-src 'none'; font-src 'self'; connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; form-action 'self'; frame-ancestors 'self'",
27+
"endpoint": undefined,
28+
"endpointName": "csp-endpoint",
29+
"isEmitting": false,
30+
"reportingEndpointsHeader": undefined,
31+
"rules": Array [
32+
"default-src 'self'",
33+
"script-src 'self'",
34+
"script-src-attr 'none'",
35+
"style-src 'self'",
36+
"child-src 'none'",
37+
"worker-src 'self'",
38+
"frame-src 'none'",
39+
"object-src 'none'",
40+
"manifest-src 'self'",
41+
"media-src 'none'",
42+
"font-src 'self'",
43+
"connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
44+
"img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
45+
"form-action 'self'",
46+
"frame-ancestors 'self'",
47+
],
48+
"useDeprecatedReportUriOnly": false,
49+
}
50+
`);
51+
});
52+
53+
test('defaults from config', () => {
54+
expect(new CspReportOnlyConfig()).toMatchInlineSnapshot(`
55+
CspReportOnlyConfig {
56+
"cspReportOnlyHeader": "default-src 'self'; script-src 'self'; script-src-attr 'none'; style-src 'self'; child-src 'none'; worker-src 'self'; frame-src 'none'; object-src 'none'; manifest-src 'self'; media-src 'none'; font-src 'self'; connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; form-action 'self'; frame-ancestors 'self'",
57+
"endpoint": undefined,
58+
"endpointName": "csp-endpoint",
59+
"isEmitting": false,
60+
"reportingEndpointsHeader": undefined,
61+
"rules": Array [
62+
"default-src 'self'",
63+
"script-src 'self'",
64+
"script-src-attr 'none'",
65+
"style-src 'self'",
66+
"child-src 'none'",
67+
"worker-src 'self'",
68+
"frame-src 'none'",
69+
"object-src 'none'",
70+
"manifest-src 'self'",
71+
"media-src 'none'",
72+
"font-src 'self'",
73+
"connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
74+
"img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
75+
"form-action 'self'",
76+
"frame-ancestors 'self'",
77+
],
78+
"useDeprecatedReportUriOnly": false,
79+
}
80+
`);
81+
});
82+
83+
test('creates from partial config', () => {
84+
expect(new CspReportOnlyConfig({ isEmitting: true })).toMatchInlineSnapshot(`
85+
CspReportOnlyConfig {
86+
"cspReportOnlyHeader": "default-src 'self'; script-src 'self'; script-src-attr 'none'; style-src 'self'; child-src 'none'; worker-src 'self'; frame-src 'none'; object-src 'none'; manifest-src 'self'; media-src 'none'; font-src 'self'; connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; form-action 'self'; frame-ancestors 'self'",
87+
"endpoint": undefined,
88+
"endpointName": "csp-endpoint",
89+
"isEmitting": true,
90+
"reportingEndpointsHeader": undefined,
91+
"rules": Array [
92+
"default-src 'self'",
93+
"script-src 'self'",
94+
"script-src-attr 'none'",
95+
"style-src 'self'",
96+
"child-src 'none'",
97+
"worker-src 'self'",
98+
"frame-src 'none'",
99+
"object-src 'none'",
100+
"manifest-src 'self'",
101+
"media-src 'none'",
102+
"font-src 'self'",
103+
"connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
104+
"img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
105+
"form-action 'self'",
106+
"frame-ancestors 'self'",
107+
],
108+
"useDeprecatedReportUriOnly": false,
109+
}
110+
`);
111+
});
112+
113+
test('computes header from rules without endpoint', () => {
114+
const cspConfig = new CspReportOnlyConfig({ rules: ['alpha', 'beta', 'gamma'] });
115+
expect(cspConfig).toMatchInlineSnapshot(`
116+
CspReportOnlyConfig {
117+
"cspReportOnlyHeader": "alpha; beta; gamma",
118+
"endpoint": undefined,
119+
"endpointName": "csp-endpoint",
120+
"isEmitting": false,
121+
"reportingEndpointsHeader": undefined,
122+
"rules": Array [
123+
"alpha",
124+
"beta",
125+
"gamma",
126+
],
127+
"useDeprecatedReportUriOnly": false,
128+
}
129+
`);
130+
});
131+
});
132+
133+
describe('when endpoint is configured with modern reporting (default)', () => {
134+
const TEST_ENDPOINT = 'https://opensearch.org/csp-endpoints';
135+
136+
test('includes both report-uri and report-to directives', () => {
137+
const config = new CspReportOnlyConfig({
138+
endpoint: TEST_ENDPOINT,
139+
isEmitting: true,
140+
});
141+
142+
expect(config).toMatchInlineSnapshot(`
143+
CspReportOnlyConfig {
144+
"cspReportOnlyHeader": "default-src 'self'; script-src 'self'; script-src-attr 'none'; style-src 'self'; child-src 'none'; worker-src 'self'; frame-src 'none'; object-src 'none'; manifest-src 'self'; media-src 'none'; font-src 'self'; connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; form-action 'self'; frame-ancestors 'self'; report-uri https://opensearch.org/csp-endpoints; report-to csp-endpoint;",
145+
"endpoint": "https://opensearch.org/csp-endpoints",
146+
"endpointName": "csp-endpoint",
147+
"isEmitting": true,
148+
"reportingEndpointsHeader": "csp-endpoint=\\"https://opensearch.org/csp-endpoints\\"",
149+
"rules": Array [
150+
"default-src 'self'",
151+
"script-src 'self'",
152+
"script-src-attr 'none'",
153+
"style-src 'self'",
154+
"child-src 'none'",
155+
"worker-src 'self'",
156+
"frame-src 'none'",
157+
"object-src 'none'",
158+
"manifest-src 'self'",
159+
"media-src 'none'",
160+
"font-src 'self'",
161+
"connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
162+
"img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
163+
"form-action 'self'",
164+
"frame-ancestors 'self'",
165+
],
166+
"useDeprecatedReportUriOnly": false,
167+
}
168+
`);
169+
});
170+
171+
test('computes header from custom rules with modern reporting', () => {
172+
const config = new CspReportOnlyConfig({
173+
rules: ['alpha', 'beta', 'gamma'],
174+
endpoint: TEST_ENDPOINT,
175+
});
176+
177+
expect(config).toMatchInlineSnapshot(`
178+
CspReportOnlyConfig {
179+
"cspReportOnlyHeader": "alpha; beta; gamma; report-uri https://opensearch.org/csp-endpoints; report-to csp-endpoint;",
180+
"endpoint": "https://opensearch.org/csp-endpoints",
181+
"endpointName": "csp-endpoint",
182+
"isEmitting": false,
183+
"reportingEndpointsHeader": "csp-endpoint=\\"https://opensearch.org/csp-endpoints\\"",
184+
"rules": Array [
185+
"alpha",
186+
"beta",
187+
"gamma",
188+
],
189+
"useDeprecatedReportUriOnly": false,
190+
}
191+
`);
192+
});
193+
194+
test('includes both reporting directives in header generation', () => {
195+
const config = new CspReportOnlyConfig({
196+
rules: ["script-src 'self'", "style-src 'self'"],
197+
endpoint: TEST_ENDPOINT,
198+
isEmitting: true,
199+
});
200+
201+
expect(config.cspReportOnlyHeader).toContain(
202+
'report-uri https://opensearch.org/csp-endpoints'
203+
);
204+
expect(config.cspReportOnlyHeader).toContain('report-to csp-endpoint');
205+
expect(config.endpoint).toBe('https://opensearch.org/csp-endpoints');
206+
expect(config.reportingEndpointsHeader).toBe(
207+
'csp-endpoint="https://opensearch.org/csp-endpoints"'
208+
);
209+
});
210+
});
211+
212+
describe('when endpoint is configured with deprecated report-uri only', () => {
213+
const TEST_ENDPOINT = 'https://opensearch.org/csp-endpoints';
214+
215+
test('includes only report-uri directive when useDeprecatedReportUriOnly is true', () => {
216+
const config = new CspReportOnlyConfig({
217+
endpoint: TEST_ENDPOINT,
218+
useDeprecatedReportUriOnly: true,
219+
isEmitting: true,
220+
});
221+
222+
expect(config).toMatchInlineSnapshot(`
223+
CspReportOnlyConfig {
224+
"cspReportOnlyHeader": "default-src 'self'; script-src 'self'; script-src-attr 'none'; style-src 'self'; child-src 'none'; worker-src 'self'; frame-src 'none'; object-src 'none'; manifest-src 'self'; media-src 'none'; font-src 'self'; connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org; form-action 'self'; frame-ancestors 'self'; report-uri https://opensearch.org/csp-endpoints;",
225+
"endpoint": "https://opensearch.org/csp-endpoints",
226+
"endpointName": "csp-endpoint",
227+
"isEmitting": true,
228+
"reportingEndpointsHeader": undefined,
229+
"rules": Array [
230+
"default-src 'self'",
231+
"script-src 'self'",
232+
"script-src-attr 'none'",
233+
"style-src 'self'",
234+
"child-src 'none'",
235+
"worker-src 'self'",
236+
"frame-src 'none'",
237+
"object-src 'none'",
238+
"manifest-src 'self'",
239+
"media-src 'none'",
240+
"font-src 'self'",
241+
"connect-src 'self' https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
242+
"img-src 'self' data: https://opensearch.org https://docs.opensearch.org https://maps.opensearch.org https://vectors.maps.opensearch.org https://tiles.maps.opensearch.org",
243+
"form-action 'self'",
244+
"frame-ancestors 'self'",
245+
],
246+
"useDeprecatedReportUriOnly": true,
247+
}
248+
`);
249+
});
250+
251+
test('computes header from custom rules with deprecated report-uri only', () => {
252+
const config = new CspReportOnlyConfig({
253+
rules: ['alpha', 'beta', 'gamma'],
254+
endpoint: TEST_ENDPOINT,
255+
useDeprecatedReportUriOnly: true,
256+
});
257+
258+
expect(config).toMatchInlineSnapshot(`
259+
CspReportOnlyConfig {
260+
"cspReportOnlyHeader": "alpha; beta; gamma; report-uri https://opensearch.org/csp-endpoints;",
261+
"endpoint": "https://opensearch.org/csp-endpoints",
262+
"endpointName": "csp-endpoint",
263+
"isEmitting": false,
264+
"reportingEndpointsHeader": undefined,
265+
"rules": Array [
266+
"alpha",
267+
"beta",
268+
"gamma",
269+
],
270+
"useDeprecatedReportUriOnly": true,
271+
}
272+
`);
273+
});
274+
275+
test('includes only report-uri directive in header generation', () => {
276+
const config = new CspReportOnlyConfig({
277+
rules: ["script-src 'self'", "style-src 'self'"],
278+
endpoint: TEST_ENDPOINT,
279+
useDeprecatedReportUriOnly: true,
280+
isEmitting: true,
281+
});
282+
283+
expect(config.cspReportOnlyHeader).toContain(
284+
'report-uri https://opensearch.org/csp-endpoints'
285+
);
286+
expect(config.cspReportOnlyHeader).not.toContain('report-to csp-endpoint');
287+
expect(config.endpoint).toBe('https://opensearch.org/csp-endpoints');
288+
expect(config.reportingEndpointsHeader).toBeUndefined();
289+
});
290+
});
291+
});

0 commit comments

Comments
 (0)