diff --git a/.changeset/fancy-lizards-grab.md b/.changeset/fancy-lizards-grab.md new file mode 100644 index 000000000000..33043be434d9 --- /dev/null +++ b/.changeset/fancy-lizards-grab.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: emit script CSP nonces when `unsafe-inline` is present if `strict-dynamic` is also present diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 925bac9103d2..6ccda0f20449 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -138,14 +138,20 @@ class BaseProvider { } /** @param {(import('types').Csp.Source | import('types').Csp.ActionSource)[] | undefined} directive */ - const needs_csp = (directive) => + const style_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); + /** @param {(import('types').Csp.Source | import('types').Csp.ActionSource)[] | undefined} directive */ + const script_needs_csp = (directive) => + !!directive && + (!directive.some((value) => value === 'unsafe-inline') || + directive.some((value) => value === 'strict-dynamic')); + + this.#script_src_needs_csp = script_needs_csp(effective_script_src); + this.#script_src_elem_needs_csp = script_needs_csp(script_src_elem); + this.#style_src_needs_csp = style_needs_csp(effective_style_src); + this.#style_src_attr_needs_csp = style_needs_csp(style_src_attr); + this.#style_src_elem_needs_csp = style_needs_csp(style_src_elem); this.#script_needs_csp = this.#script_src_needs_csp || this.#script_src_elem_needs_csp; this.#style_needs_csp = diff --git a/packages/kit/src/runtime/server/page/csp.spec.js b/packages/kit/src/runtime/server/page/csp.spec.js index 301e708e0d9f..d8c6e91d025d 100644 --- a/packages/kit/src/runtime/server/page/csp.spec.js +++ b/packages/kit/src/runtime/server/page/csp.spec.js @@ -408,4 +408,70 @@ describe.skipIf(process.env.NODE_ENV !== 'production')('CSPs in prod', () => { ); }, '`content-security-policy-report-only` must be specified with either the `report-to` or `report-uri` directives, or both'); }); + + test('adds nonce when both unsafe-inline and strict-dynamic are present', () => { + const csp = new Csp( + { + mode: 'nonce', + directives: { + 'script-src': ['strict-dynamic', 'unsafe-inline'] + }, + reportOnly: {} + }, + { prerender: false } + ); + + csp.add_script(''); + + const header = csp.csp_provider.get_header(); + // Should include nonce even though unsafe-inline is present, + // because strict-dynamic causes browsers to ignore unsafe-inline + assert.ok(header.includes("'nonce-")); + assert.ok(header.includes("'strict-dynamic'")); + assert.ok(header.includes("'unsafe-inline'")); + }); + + test('adds nonce with strict-dynamic in default-src', () => { + const csp = new Csp( + { + mode: 'nonce', + directives: { + 'default-src': ['strict-dynamic', 'unsafe-inline'] + }, + reportOnly: {} + }, + { prerender: false } + ); + + csp.add_script(''); + + const header = csp.csp_provider.get_header(); + // Should include nonce even though unsafe-inline is present + assert.ok(header.includes("'nonce-")); + assert.ok(header.includes("'strict-dynamic'")); + assert.ok(header.includes("'unsafe-inline'")); + }); + + test('strict-dynamic does not affect style-src', () => { + const csp = new Csp( + { + mode: 'nonce', + directives: { + // strict-dynamic only affects scripts, not styles per CSP spec + 'style-src': ['strict-dynamic', 'unsafe-inline'] + }, + reportOnly: {} + }, + { prerender: false } + ); + + csp.add_style(''); + + const header = csp.csp_provider.get_header(); + // Should NOT include nonce because strict-dynamic doesn't affect styles + // and unsafe-inline is present + assert.ok(!header.includes("'nonce-")); + assert.ok(header.includes("'strict-dynamic'")); + assert.ok(header.includes("'unsafe-inline'")); + }); });