Skip to content
5 changes: 5 additions & 0 deletions .changeset/giant-years-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": patch
---

fix: add nonce or hash to "script-src-elem", "style-src-attr" and "style-src-elem" if defined in CSP config
114 changes: 97 additions & 17 deletions packages/kit/src/runtime/server/page/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,18 @@ class BaseProvider {
/** @type {import('types').Csp.Source[]} */
#script_src;

/** @type {import('types').Csp.Source[]} */
#script_src_elem;

/** @type {import('types').Csp.Source[]} */
#style_src;

/** @type {import('types').Csp.Source[]} */
#style_src_attr;

/** @type {import('types').Csp.Source[]} */
#style_src_elem;

/** @type {string} */
#nonce;

Expand All @@ -57,6 +66,18 @@ class BaseProvider {

const d = this.#directives;

this.#script_src = [];
this.#script_src_elem = [];
this.#style_src = [];
this.#style_src_attr = [];
this.#style_src_elem = [];

const effective_script_src = d['script-src'] || d['default-src'];
const script_src_elem = d['script-src-elem'];
const effective_style_src = d['style-src'] || d['default-src'];
const style_src_attr = d['style-src-attr'];
const style_src_elem = d['style-src-elem'];

if (__SVELTEKIT_DEV__) {
// remove strict-dynamic in dev...
// TODO reinstate this if we can figure out how to make strict-dynamic work
Expand All @@ -70,28 +91,34 @@ class BaseProvider {
// if (d['script-src'].length === 0) delete d['script-src'];
// }

const effective_style_src = d['style-src'] || d['default-src'];

// ...and add unsafe-inline so we can inject <style> elements
if (effective_style_src && !effective_style_src.includes('unsafe-inline')) {
d['style-src'] = [...effective_style_src, 'unsafe-inline'];
}
}

this.#script_src = [];
this.#style_src = [];
if (style_src_attr && !style_src_attr.includes('unsafe-inline')) {
d['style-src-attr'] = [...style_src_attr, 'unsafe-inline'];
}

const effective_script_src = d['script-src'] || d['default-src'];
const effective_style_src = d['style-src'] || d['default-src'];
if (style_src_elem && !style_src_elem.includes('unsafe-inline')) {
d['style-src-elem'] = [...style_src_elem, 'unsafe-inline'];
}
}

this.#script_needs_csp =
!!effective_script_src &&
effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0;
(!!effective_script_src &&
effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0) ||
(!!script_src_elem &&
script_src_elem.filter((value) => value !== 'unsafe-inline').length > 0);

this.#style_needs_csp =
!__SVELTEKIT_DEV__ &&
!!effective_style_src &&
effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0;
((!!effective_style_src &&
effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0) ||
(!!style_src_attr &&
style_src_attr.filter((value) => value !== 'unsafe-inline').length > 0) ||
(!!style_src_elem &&
style_src_elem.filter((value) => value !== 'unsafe-inline').length > 0));

this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes;
this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes;
Expand All @@ -101,21 +128,53 @@ class BaseProvider {
/** @param {string} content */
add_script(content) {
if (this.#script_needs_csp) {
const d = this.#directives;

if (this.#use_hashes) {
this.#script_src.push(`sha256-${sha256(content)}`);
} else if (this.#script_src.length === 0) {
this.#script_src.push(`nonce-${this.#nonce}`);
const hash = sha256(content);

this.#script_src.push(`sha256-${hash}`);

if (d['script-src-elem']?.length) {
this.#script_src_elem.push(`sha256-${hash}`);
}
} else {
if (this.#script_src.length === 0) {
this.#script_src.push(`nonce-${this.#nonce}`);
}
if (d['script-src-elem']?.length) {
this.#script_src_elem.push(`nonce-${this.#nonce}`);
}
}
}
}

/** @param {string} content */
add_style(content) {
if (this.#style_needs_csp) {
const d = this.#directives;

if (this.#use_hashes) {
this.#style_src.push(`sha256-${sha256(content)}`);
} else if (this.#style_src.length === 0) {
this.#style_src.push(`nonce-${this.#nonce}`);
const hash = sha256(content);

this.#style_src.push(`sha256-${hash}`);

if (d['style-src-attr']?.length) {
this.#style_src_attr.push(`sha256-${hash}`);
}
if (d['style-src-elem']?.length) {
this.#style_src_elem.push(`sha256-${hash}`);
}
} else {
if (this.#style_src.length === 0) {
this.#style_src.push(`nonce-${this.#nonce}`);
}
if (d['style-src-attr']?.length) {
this.#style_src_attr.push(`nonce-${this.#nonce}`);
}
if (d['style-src-elem']?.length) {
this.#style_src_elem.push(`nonce-${this.#nonce}`);
}
}
}
}
Expand All @@ -139,13 +198,34 @@ class BaseProvider {
];
}

if (this.#style_src_attr.length > 0) {
directives['style-src-attr'] = [
...(directives['style-src-attr'] || []),
...this.#style_src_attr
];
}

if (this.#style_src_elem.length > 0) {
directives['style-src-elem'] = [
...(directives['style-src-elem'] || []),
...this.#style_src_elem
];
}

if (this.#script_src.length > 0) {
directives['script-src'] = [
...(directives['script-src'] || directives['default-src'] || []),
...this.#script_src
];
}

if (this.#script_src_elem.length > 0) {
directives['script-src-elem'] = [
...(directives['script-src-elem'] || []),
...this.#script_src_elem
];
}

for (const key in directives) {
if (is_meta && (key === 'frame-ancestors' || key === 'report-uri' || key === 'sandbox')) {
// these values cannot be used with a <meta> tag
Expand Down
72 changes: 69 additions & 3 deletions packages/kit/src/runtime/server/page/csp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,68 @@ test('skips frame-ancestors, report-uri, sandbox from meta tags', () => {
);
});

test('adds nonce to script-src-elem, style-src-attr and style-src-elem if necessary', () => {
const csp = new Csp(
{
mode: 'auto',
directives: {
'script-src-elem': ['self'],
'style-src-attr': ['self'],
'style-src-elem': ['self']
},
reportOnly: {}
},
{
prerender: false
}
);

csp.add_script('');
csp.add_style('');

const csp_header = csp.csp_provider.get_header();
assert.ok(csp_header.includes("script-src-elem 'self' 'nonce-"));
assert.ok(csp_header.includes("style-src-attr 'self' 'nonce-"));
assert.ok(csp_header.includes("style-src-elem 'self' 'nonce-"));
});

test('adds hash to script-src-elem, style-src-attr and style-src-elem if necessary during prerendering', () => {
const csp = new Csp(
{
mode: 'auto',
directives: {
'script-src-elem': ['self'],
'style-src-attr': ['self'],
'style-src-elem': ['self']
},
reportOnly: {}
},
{
prerender: true
}
);

csp.add_script('');
csp.add_style('');

const csp_header = csp.csp_provider.get_header();
assert.ok(
csp_header.includes(
"script-src-elem 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
)
);
assert.ok(
csp_header.includes(
"style-src-attr 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
)
);
assert.ok(
csp_header.includes(
"style-src-elem 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"
)
);
});

test('adds unsafe-inline styles in dev', () => {
// @ts-expect-error
globalThis.__SVELTEKIT_DEV__ = true;
Expand All @@ -161,10 +223,14 @@ test('adds unsafe-inline styles in dev', () => {
{
mode: 'hash',
directives: {
'default-src': ['self']
'default-src': ['self'],
'style-src-attr': ['self'],
'style-src-elem': ['self']
},
reportOnly: {
'default-src': ['self'],
'style-src-attr': ['self'],
'style-src-elem': ['self'],
'report-uri': ['/']
}
},
Expand All @@ -177,12 +243,12 @@ test('adds unsafe-inline styles in dev', () => {

assert.equal(
csp.csp_provider.get_header(),
"default-src 'self'; style-src 'self' 'unsafe-inline'"
"default-src 'self'; style-src-attr 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);

assert.equal(
csp.report_only_provider.get_header(),
"default-src 'self'; report-uri /; style-src 'self' 'unsafe-inline'"
"default-src 'self'; style-src-attr 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; report-uri /; style-src 'self' 'unsafe-inline'"
);
});

Expand Down