Skip to content

Commit

Permalink
fix: csp nonce in script-src-elem, style-src-attr and `style-src-…
Browse files Browse the repository at this point in the history
…elem` when using unsafe-inline (#11613)

* 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

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

* fix test desc

* Update giant-years-drum.md

* refactor

* rename variable

* avoid duplicate empty comment hash

* add back removed test

* added back another test

* add back skips nonce in style-src when using unsafe-inline test

* put tests in same order

---------

Co-authored-by: Rich Harris <hello@rich-harris.dev>
Co-authored-by: Chew Tee Ming <chew.tee.ming@nindatech.com>
  • Loading branch information
3 people authored Nov 13, 2024
1 parent c717db9 commit 6df00fc
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 67 deletions.
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', () => {
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

0 comments on commit 6df00fc

Please sign in to comment.