Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/quiet-nonces-stay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: add the CSP nonce for scripts in `app.html` to `script-src` when a page has `csr = false`
8 changes: 7 additions & 1 deletion packages/kit/src/runtime/server/page/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,16 @@ export class Csp {
* @param {import('./types.js').CspConfig} config
* @param {import('./types.js').CspOpts} opts
*/
constructor({ mode, directives, reportOnly }, { prerender }) {
constructor({ mode, directives, reportOnly }, { prerender, template_contains_nonce }) {
this.#use_hashes = mode === 'hash' || (mode === 'auto' && prerender);
this.csp_provider = new CspProvider(this.#use_hashes, directives);
this.report_only_provider = new CspReportOnlyProvider(this.#use_hashes, reportOnly);

// scripts in the template can use the nonce whether or not the page adds any of its own
if (template_contains_nonce && !this.#use_hashes) {
this.csp_provider.add_script(`nonce-${this.nonce}`);
this.report_only_provider.add_script(`nonce-${this.nonce}`);
}
}

/**
Expand Down
70 changes: 70 additions & 0 deletions packages/kit/src/runtime/server/page/csp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,76 @@ describe('CSPs in prod', () => {
assert.ok(csp.nonce);
});

/** @type {Array<'nonce' | 'auto'>} */
const nonce_modes = ['nonce', 'auto'];

test('adds the nonce for scripts in the template when the page adds no script', () => {
for (const mode of nonce_modes) {
const csp = new Csp(
{
mode,
directives: {
'script-src': ['self']
},
reportOnly: {
'script-src': ['self'],
'report-uri': ['/']
}
},
{
prerender: false,
template_contains_nonce: true
}
);

assert.equal(csp.csp_provider.get_header(), `script-src 'self' 'nonce-${csp.nonce}'`);
assert.equal(
csp.report_only_provider.get_header(),
`script-src 'self' 'nonce-${csp.nonce}'; report-uri /`
);
}
});

test('does not add a nonce when neither the template nor the page uses one', () => {
for (const mode of nonce_modes) {
for (const template_contains_nonce of [false, undefined]) {
const csp = new Csp(
{
mode,
directives: {
'script-src': ['self']
},
reportOnly: {}
},
{
prerender: false,
template_contains_nonce
}
);

assert.equal(csp.csp_provider.get_header(), "script-src 'self'");
}
}
});

test('does not add the template nonce when using hashes', () => {
const csp = new Csp(
{
mode: 'hash',
directives: {
'script-src': ['self']
},
reportOnly: {}
},
{
prerender: false,
template_contains_nonce: true
}
);

assert.equal(csp.csp_provider.get_header(), "script-src 'self'");
});

test('throws when reportOnly contains directives but no report-uri or report-to', () => {
expect(() => {
new Csp(
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export async function render_response({
let base_expression = s(paths.base);

const csp = new Csp(options.csp, {
prerender: !!(state.prerendering || state.prerender_default === true)
prerender: !!(state.prerendering || state.prerender_default === true),
template_contains_nonce: options.app_template_contains_nonce
});

// if appropriate, use relative paths for greater portability
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/server/page/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface CspConfig {

export interface CspOpts {
prerender: boolean;
/** Whether the app template contains `%sveltekit.nonce%` */
template_contains_nonce?: boolean;
}

export interface Cookie {
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/test/apps/no-csr/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script nonce="%sveltekit.nonce%">
document.documentElement.dataset.templateScript = 'ran';
</script>
%sveltekit.head%
</head>
<body>
Expand Down
12 changes: 10 additions & 2 deletions packages/kit/test/apps/no-csr/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import process from 'node:process';
import { expect } from '@playwright/test';
import { test } from '../../../utils.js';

test.skip(({ javaScriptEnabled }) => javaScriptEnabled);

test.describe.configure({ mode: 'parallel' });

test.describe('Skipped client build', () => {
test.skip(({ javaScriptEnabled }) => javaScriptEnabled);
test.skip(!!process.env.DEV);

test('omits client build output', async () => {
Expand Down Expand Up @@ -39,3 +38,12 @@ test.describe('Skipped client build', () => {
expect(worker_response.status()).toBe(200);
});
});

test.describe('CSP', () => {
test.skip(({ javaScriptEnabled }) => !javaScriptEnabled);

test('runs the scripts in the template that use the nonce', async ({ page }) => {
await page.goto('/assets', { wait_for_started: false });
await expect(page.locator('html')).toHaveAttribute('data-template-script', 'ran');
});
});
Loading