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

fix: csp nonce in script-src-elem, style-src-attr and style-src-elem when using unsafe-inline #11613

Merged
merged 25 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7d80e20
add nonce to script-src-elem csp directive if defined
MathiasWP Dec 29, 2023
c4cf0ef
added changeset
MathiasWP Dec 29, 2023
b168c24
also handle hashes and style-src-attr and style-src-elem
MathiasWP Dec 29, 2023
72156a0
changed order of variable declaration
MathiasWP Dec 29, 2023
12855e7
fixed typo
MathiasWP Dec 29, 2023
403ab86
updated changeset
MathiasWP Dec 29, 2023
e1e0795
fix bug and update test
MathiasWP Dec 29, 2023
a6d8419
update test
MathiasWP Dec 29, 2023
aabaee2
write better tests and fix bugs
MathiasWP Dec 30, 2023
1d961f1
Update .changeset/giant-years-drum.md
Rich-Harris Jan 8, 2024
d59e339
Merge branch 'sveltejs:main' into main
MathiasWP Jan 9, 2024
4fc13c6
Merge branch 'sveltejs:main' into main
MathiasWP Jan 11, 2024
5365a94
fix csp nonce in script-src-elem, style-src-attr and style-src-elem w…
MathiasWP Jan 11, 2024
195d271
fix test desc
MathiasWP Jan 11, 2024
74e8a93
Update giant-years-drum.md
MathiasWP Jan 11, 2024
d46d29e
refactor
MathiasWP Jan 11, 2024
5842ed5
Merge branch 'csp-unsafe-inline' of github.com:MathiasWP/kit into csp…
MathiasWP Jan 11, 2024
b55ee1c
rename variable
MathiasWP Jan 11, 2024
eba0e81
avoid duplicate empty comment hash
MathiasWP Jan 11, 2024
d3db232
Merge branch 'main' into pr/MathiasWP/11613
eltigerchino Oct 22, 2024
b5d3e10
add back removed test
MathiasWP Nov 12, 2024
5ecaf3b
Merge branch 'csp-unsafe-inline' of github.com:MathiasWP/kit into csp…
MathiasWP Nov 12, 2024
c78fe33
added back another test
MathiasWP Nov 12, 2024
06ca1de
add back skips nonce in style-src when using unsafe-inline test
MathiasWP Nov 12, 2024
8be42e1
put tests in same order
MathiasWP Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: only add nonce to `script-src-elem`, `style-src-attr` and `style-src-elem` CSP directives when `unsafe-inline` is not present
130 changes: 67 additions & 63 deletions packages/kit/src/runtime/server/page/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,24 @@ class BaseProvider {
/** @type {boolean} */
#script_needs_csp;

/** @type {boolean} */
#script_src_needs_csp;

/** @type {boolean} */
#script_src_elem_needs_csp;

/** @type {boolean} */
#style_needs_csp;

/** @type {boolean} */
#style_src_needs_csp;

/** @type {boolean} */
#style_src_attr_needs_csp;

/** @type {boolean} */
#style_src_elem_needs_csp;

/** @type {import('types').CspDirectives} */
#directives;

Expand Down Expand Up @@ -121,92 +136,81 @@ class BaseProvider {
}
}

this.#script_needs_csp =
(!!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);
/** @param {(import('types').Csp.Source | import('types').Csp.ActionSource)[] | undefined} directive */
const needs_csp = (directive) =>
!!directive && !directive.some((value) => value === 'unsafe-inline');

this.#script_src_needs_csp = needs_csp(effective_script_src);
this.#script_src_elem_needs_csp = needs_csp(script_src_elem);
this.#style_src_needs_csp = needs_csp(effective_style_src);
this.#style_src_attr_needs_csp = needs_csp(style_src_attr);
this.#style_src_elem_needs_csp = needs_csp(style_src_elem);

this.#script_needs_csp = this.#script_src_needs_csp || this.#script_src_elem_needs_csp;
this.#style_needs_csp =
!__SVELTEKIT_DEV__ &&
((!!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.#style_src_needs_csp ||
this.#style_src_attr_needs_csp ||
this.#style_src_elem_needs_csp);

this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes;
this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes;

this.#nonce = nonce;
}

/** @param {string} content */
add_script(content) {
if (this.#script_needs_csp) {
const d = this.#directives;
if (!this.#script_needs_csp) return;

if (this.#use_hashes) {
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}`);
}
}
/** @type {`nonce-${string}` | `sha256-${string}`} */
const source = this.#use_hashes ? `sha256-${sha256(content)}` : `nonce-${this.#nonce}`;

if (this.#script_src_needs_csp) {
this.#script_src.push(source);
}

if (this.#script_src_elem_needs_csp) {
this.#script_src_elem.push(source);
}
}

/** @param {string} content */
add_style(content) {
if (this.#style_needs_csp) {
// this is the hash for "/* empty */"
// adding it so that svelte does not break csp
// see https://github.com/sveltejs/svelte/pull/7800
const empty_comment_hash = '9OlNO0DNEeaVzHL4RZwCLsBHA8WBQ8toBp/4F5XV2nc=';
if (!this.#style_needs_csp) return;

const d = this.#directives;
/** @type {`nonce-${string}` | `sha256-${string}`} */
const source = this.#use_hashes ? `sha256-${sha256(content)}` : `nonce-${this.#nonce}`;

if (this.#use_hashes) {
const hash = sha256(content);
if (this.#style_src_needs_csp) {
this.#style_src.push(source);
}

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

if (d['style-src-attr']?.length) {
this.#style_src_attr.push(`sha256-${hash}`);
}
if (d['style-src-elem']?.length) {
if (
hash !== empty_comment_hash &&
!d['style-src-elem'].includes(`sha256-${empty_comment_hash}`)
) {
this.#style_src_elem.push(`sha256-${empty_comment_hash}`);
}
if (this.#style_src_attr_needs_csp) {
this.#style_src_attr.push(source);
}

this.#style_src_elem.push(`sha256-${hash}`);
}
} else {
if (this.#style_src.length === 0 && !d['style-src']?.includes('unsafe-inline')) {
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) {
if (!d['style-src-elem'].includes(`sha256-${empty_comment_hash}`)) {
this.#style_src_elem.push(`sha256-${empty_comment_hash}`);
}
if (this.#style_src_elem_needs_csp) {
// this is the sha256 hash for the string "/* empty */"
// adding it so that svelte does not break csp
// see https://github.com/sveltejs/svelte/pull/7800
const sha256_empty_comment_hash = 'sha256-9OlNO0DNEeaVzHL4RZwCLsBHA8WBQ8toBp/4F5XV2nc=';
const d = this.#directives;

if (
d['style-src-elem'] &&
!d['style-src-elem'].includes(sha256_empty_comment_hash) &&
!this.#style_src_elem.includes(sha256_empty_comment_hash)
) {
this.#style_src_elem.push(sha256_empty_comment_hash);
}

this.#style_src_elem.push(`nonce-${this.#nonce}`);
}
if (source !== sha256_empty_comment_hash) {
this.#style_src_elem.push(source);
}
}
}
Expand Down
49 changes: 45 additions & 4 deletions packages/kit/src/runtime/server/page/csp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,20 @@ test('skips nonce with unsafe-inline', () => {
{
mode: 'nonce',
directives: {
'default-src': ['unsafe-inline']
'default-src': ['unsafe-inline'],
'script-src': ['unsafe-inline'],
'script-src-elem': ['unsafe-inline'],
'style-src': ['unsafe-inline'],
'style-src-attr': ['unsafe-inline'],
'style-src-elem': ['unsafe-inline']
},
reportOnly: {
'default-src': ['unsafe-inline'],
'script-src': ['unsafe-inline'],
'script-src-elem': ['unsafe-inline'],
'style-src': ['unsafe-inline'],
'style-src-attr': ['unsafe-inline'],
'style-src-elem': ['unsafe-inline'],
'report-uri': ['/']
}
},
Expand All @@ -97,9 +107,16 @@ test('skips nonce with unsafe-inline', () => {
);

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

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

test('skips nonce in style-src when using unsafe-inline', () => {
MathiasWP marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -151,6 +168,30 @@ test('skips hash with unsafe-inline', () => {
assert.equal(csp.report_only_provider.get_header(), "default-src 'unsafe-inline'; report-uri /");
});

test('does not add empty comment hash to style-src-elem if already defined', () => {
const csp = new Csp(
{
mode: 'hash',
directives: {
'style-src-elem': ['self', 'sha256-9OlNO0DNEeaVzHL4RZwCLsBHA8WBQ8toBp/4F5XV2nc=']
},
reportOnly: {
'report-uri': ['/']
}
},
{
prerender: false
}
);

csp.add_style('/* empty */');

assert.equal(
csp.csp_provider.get_header(),
"style-src-elem 'self' 'sha256-9OlNO0DNEeaVzHL4RZwCLsBHA8WBQ8toBp/4F5XV2nc='"
);
});

test('skips frame-ancestors, report-uri, sandbox from meta tags', () => {
const csp = new Csp(
{
Expand Down Expand Up @@ -179,7 +220,7 @@ 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', () => {
test('adds nonce style-src-attr and style-src-elem and nonce + sha to script-src-elem if necessary', () => {
const csp = new Csp(
{
mode: 'auto',
Expand Down
Loading