Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[security]: automatically add nonce or hash to script-src-elem, style-src-attr & style-src-elem csp directive if necessary #11485

Merged
merged 10 commits into from
Jan 8, 2024
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
---

nonce or hash is automatically added to "script-src-elem", "style-src-attr" and "style-src-elem" if defined in csp directives config
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
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
Loading