Skip to content

Commit 1a1e786

Browse files
[security]: automatically add nonce or hash to script-src-elem, style-src-attr & style-src-elem csp directive if necessary (#11485)
* add nonce to script-src-elem csp directive if defined * added changeset * also handle hashes and style-src-attr and style-src-elem * changed order of variable declaration * fixed typo * updated changeset * fix bug and update test * update test * write better tests and fix bugs * Update .changeset/giant-years-drum.md --------- Co-authored-by: Rich Harris <hello@rich-harris.dev>
1 parent 099608c commit 1a1e786

File tree

3 files changed

+171
-20
lines changed

3 files changed

+171
-20
lines changed

.changeset/giant-years-drum.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": patch
3+
---
4+
5+
fix: add nonce or hash to "script-src-elem", "style-src-attr" and "style-src-elem" if defined in CSP config

packages/kit/src/runtime/server/page/csp.js

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,18 @@ class BaseProvider {
4040
/** @type {import('types').Csp.Source[]} */
4141
#script_src;
4242

43+
/** @type {import('types').Csp.Source[]} */
44+
#script_src_elem;
45+
4346
/** @type {import('types').Csp.Source[]} */
4447
#style_src;
4548

49+
/** @type {import('types').Csp.Source[]} */
50+
#style_src_attr;
51+
52+
/** @type {import('types').Csp.Source[]} */
53+
#style_src_elem;
54+
4655
/** @type {string} */
4756
#nonce;
4857

@@ -57,6 +66,18 @@ class BaseProvider {
5766

5867
const d = this.#directives;
5968

69+
this.#script_src = [];
70+
this.#script_src_elem = [];
71+
this.#style_src = [];
72+
this.#style_src_attr = [];
73+
this.#style_src_elem = [];
74+
75+
const effective_script_src = d['script-src'] || d['default-src'];
76+
const script_src_elem = d['script-src-elem'];
77+
const effective_style_src = d['style-src'] || d['default-src'];
78+
const style_src_attr = d['style-src-attr'];
79+
const style_src_elem = d['style-src-elem'];
80+
6081
if (__SVELTEKIT_DEV__) {
6182
// remove strict-dynamic in dev...
6283
// TODO reinstate this if we can figure out how to make strict-dynamic work
@@ -70,28 +91,34 @@ class BaseProvider {
7091
// if (d['script-src'].length === 0) delete d['script-src'];
7192
// }
7293

73-
const effective_style_src = d['style-src'] || d['default-src'];
74-
7594
// ...and add unsafe-inline so we can inject <style> elements
7695
if (effective_style_src && !effective_style_src.includes('unsafe-inline')) {
7796
d['style-src'] = [...effective_style_src, 'unsafe-inline'];
7897
}
79-
}
8098

81-
this.#script_src = [];
82-
this.#style_src = [];
99+
if (style_src_attr && !style_src_attr.includes('unsafe-inline')) {
100+
d['style-src-attr'] = [...style_src_attr, 'unsafe-inline'];
101+
}
83102

84-
const effective_script_src = d['script-src'] || d['default-src'];
85-
const effective_style_src = d['style-src'] || d['default-src'];
103+
if (style_src_elem && !style_src_elem.includes('unsafe-inline')) {
104+
d['style-src-elem'] = [...style_src_elem, 'unsafe-inline'];
105+
}
106+
}
86107

87108
this.#script_needs_csp =
88-
!!effective_script_src &&
89-
effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0;
109+
(!!effective_script_src &&
110+
effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0) ||
111+
(!!script_src_elem &&
112+
script_src_elem.filter((value) => value !== 'unsafe-inline').length > 0);
90113

91114
this.#style_needs_csp =
92115
!__SVELTEKIT_DEV__ &&
93-
!!effective_style_src &&
94-
effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0;
116+
((!!effective_style_src &&
117+
effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0) ||
118+
(!!style_src_attr &&
119+
style_src_attr.filter((value) => value !== 'unsafe-inline').length > 0) ||
120+
(!!style_src_elem &&
121+
style_src_elem.filter((value) => value !== 'unsafe-inline').length > 0));
95122

96123
this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes;
97124
this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes;
@@ -101,21 +128,53 @@ class BaseProvider {
101128
/** @param {string} content */
102129
add_script(content) {
103130
if (this.#script_needs_csp) {
131+
const d = this.#directives;
132+
104133
if (this.#use_hashes) {
105-
this.#script_src.push(`sha256-${sha256(content)}`);
106-
} else if (this.#script_src.length === 0) {
107-
this.#script_src.push(`nonce-${this.#nonce}`);
134+
const hash = sha256(content);
135+
136+
this.#script_src.push(`sha256-${hash}`);
137+
138+
if (d['script-src-elem']?.length) {
139+
this.#script_src_elem.push(`sha256-${hash}`);
140+
}
141+
} else {
142+
if (this.#script_src.length === 0) {
143+
this.#script_src.push(`nonce-${this.#nonce}`);
144+
}
145+
if (d['script-src-elem']?.length) {
146+
this.#script_src_elem.push(`nonce-${this.#nonce}`);
147+
}
108148
}
109149
}
110150
}
111151

112152
/** @param {string} content */
113153
add_style(content) {
114154
if (this.#style_needs_csp) {
155+
const d = this.#directives;
156+
115157
if (this.#use_hashes) {
116-
this.#style_src.push(`sha256-${sha256(content)}`);
117-
} else if (this.#style_src.length === 0) {
118-
this.#style_src.push(`nonce-${this.#nonce}`);
158+
const hash = sha256(content);
159+
160+
this.#style_src.push(`sha256-${hash}`);
161+
162+
if (d['style-src-attr']?.length) {
163+
this.#style_src_attr.push(`sha256-${hash}`);
164+
}
165+
if (d['style-src-elem']?.length) {
166+
this.#style_src_elem.push(`sha256-${hash}`);
167+
}
168+
} else {
169+
if (this.#style_src.length === 0) {
170+
this.#style_src.push(`nonce-${this.#nonce}`);
171+
}
172+
if (d['style-src-attr']?.length) {
173+
this.#style_src_attr.push(`nonce-${this.#nonce}`);
174+
}
175+
if (d['style-src-elem']?.length) {
176+
this.#style_src_elem.push(`nonce-${this.#nonce}`);
177+
}
119178
}
120179
}
121180
}
@@ -139,13 +198,34 @@ class BaseProvider {
139198
];
140199
}
141200

201+
if (this.#style_src_attr.length > 0) {
202+
directives['style-src-attr'] = [
203+
...(directives['style-src-attr'] || []),
204+
...this.#style_src_attr
205+
];
206+
}
207+
208+
if (this.#style_src_elem.length > 0) {
209+
directives['style-src-elem'] = [
210+
...(directives['style-src-elem'] || []),
211+
...this.#style_src_elem
212+
];
213+
}
214+
142215
if (this.#script_src.length > 0) {
143216
directives['script-src'] = [
144217
...(directives['script-src'] || directives['default-src'] || []),
145218
...this.#script_src
146219
];
147220
}
148221

222+
if (this.#script_src_elem.length > 0) {
223+
directives['script-src-elem'] = [
224+
...(directives['script-src-elem'] || []),
225+
...this.#script_src_elem
226+
];
227+
}
228+
149229
for (const key in directives) {
150230
if (is_meta && (key === 'frame-ancestors' || key === 'report-uri' || key === 'sandbox')) {
151231
// these values cannot be used with a <meta> tag

packages/kit/src/runtime/server/page/csp.spec.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,68 @@ test('skips frame-ancestors, report-uri, sandbox from meta tags', () => {
153153
);
154154
});
155155

156+
test('adds nonce to script-src-elem, style-src-attr and style-src-elem if necessary', () => {
157+
const csp = new Csp(
158+
{
159+
mode: 'auto',
160+
directives: {
161+
'script-src-elem': ['self'],
162+
'style-src-attr': ['self'],
163+
'style-src-elem': ['self']
164+
},
165+
reportOnly: {}
166+
},
167+
{
168+
prerender: false
169+
}
170+
);
171+
172+
csp.add_script('');
173+
csp.add_style('');
174+
175+
const csp_header = csp.csp_provider.get_header();
176+
assert.ok(csp_header.includes("script-src-elem 'self' 'nonce-"));
177+
assert.ok(csp_header.includes("style-src-attr 'self' 'nonce-"));
178+
assert.ok(csp_header.includes("style-src-elem 'self' 'nonce-"));
179+
});
180+
181+
test('adds hash to script-src-elem, style-src-attr and style-src-elem if necessary during prerendering', () => {
182+
const csp = new Csp(
183+
{
184+
mode: 'auto',
185+
directives: {
186+
'script-src-elem': ['self'],
187+
'style-src-attr': ['self'],
188+
'style-src-elem': ['self']
189+
},
190+
reportOnly: {}
191+
},
192+
{
193+
prerender: true
194+
}
195+
);
196+
197+
csp.add_script('');
198+
csp.add_style('');
199+
200+
const csp_header = csp.csp_provider.get_header();
201+
assert.ok(
202+
csp_header.includes(
203+
"script-src-elem 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
204+
)
205+
);
206+
assert.ok(
207+
csp_header.includes(
208+
"style-src-attr 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
209+
)
210+
);
211+
assert.ok(
212+
csp_header.includes(
213+
"style-src-elem 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
214+
)
215+
);
216+
});
217+
156218
test('adds unsafe-inline styles in dev', () => {
157219
// @ts-expect-error
158220
globalThis.__SVELTEKIT_DEV__ = true;
@@ -161,10 +223,14 @@ test('adds unsafe-inline styles in dev', () => {
161223
{
162224
mode: 'hash',
163225
directives: {
164-
'default-src': ['self']
226+
'default-src': ['self'],
227+
'style-src-attr': ['self'],
228+
'style-src-elem': ['self']
165229
},
166230
reportOnly: {
167231
'default-src': ['self'],
232+
'style-src-attr': ['self'],
233+
'style-src-elem': ['self'],
168234
'report-uri': ['/']
169235
}
170236
},
@@ -177,12 +243,12 @@ test('adds unsafe-inline styles in dev', () => {
177243

178244
assert.equal(
179245
csp.csp_provider.get_header(),
180-
"default-src 'self'; style-src 'self' 'unsafe-inline'"
246+
"default-src 'self'; style-src-attr 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
181247
);
182248

183249
assert.equal(
184250
csp.report_only_provider.get_header(),
185-
"default-src 'self'; report-uri /; style-src 'self' 'unsafe-inline'"
251+
"default-src 'self'; style-src-attr 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; report-uri /; style-src 'self' 'unsafe-inline'"
186252
);
187253
});
188254

0 commit comments

Comments
 (0)