diff --git a/.changeset/giant-years-drum.md b/.changeset/giant-years-drum.md new file mode 100644 index 000000000000..e1129e0d0486 --- /dev/null +++ b/.changeset/giant-years-drum.md @@ -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 diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 547c43cd8835..336d50261ad3 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -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; @@ -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); } } } diff --git a/packages/kit/src/runtime/server/page/csp.spec.js b/packages/kit/src/runtime/server/page/csp.spec.js index 670d27305065..6dc1a9435856 100644 --- a/packages/kit/src/runtime/server/page/csp.spec.js +++ b/packages/kit/src/runtime/server/page/csp.spec.js @@ -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': ['/'] } }, @@ -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', () => { @@ -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( { @@ -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',