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/silent-sites-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: `config.kit.csp.directives['trusted-types']` requires `'svelte-trusted-html'` (and `'sveltekit-trusted-url'` when a service worker is automatically registered) if it is configured
24 changes: 22 additions & 2 deletions packages/kit/src/core/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path';
import process from 'node:process';
import * as url from 'node:url';
import options from './options.js';
import { resolve_entry } from '../../utils/filesystem.js';

/**
* Loads the template (src/app.html by default) and validates that it has the
Expand Down Expand Up @@ -96,7 +97,7 @@ export async function load_config({ cwd = process.cwd() } = {}) {
* @returns {import('types').ValidatedConfig}
*/
function process_config(config, { cwd = process.cwd() } = {}) {
const validated = validate_config(config);
const validated = validate_config(config, cwd);

validated.kit.outDir = path.resolve(cwd, validated.kit.outDir);

Expand All @@ -116,15 +117,17 @@ function process_config(config, { cwd = process.cwd() } = {}) {

/**
* @param {import('@sveltejs/kit').Config} config
* @param {string} [cwd]
* @returns {import('types').ValidatedConfig}
*/
export function validate_config(config) {
export function validate_config(config, cwd = process.cwd()) {
if (typeof config !== 'object') {
throw new Error(
'The Svelte config file must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
);
}

/** @type {import('types').ValidatedConfig} */
const validated = options(config, 'config');
const files = validated.kit.files;

Expand All @@ -151,5 +154,22 @@ export function validate_config(config) {
}
}

if (validated.kit.csp?.directives?.['require-trusted-types-for']?.includes('script')) {
if (!validated.kit.csp?.directives?.['trusted-types']?.includes('svelte-trusted-html')) {
throw new Error(
"The `csp.directives['trusted-types']` option must include 'svelte-trusted-html'"
);
}
if (
validated.kit.serviceWorker?.register &&
resolve_entry(path.resolve(cwd, validated.kit.files.serviceWorker)) &&
!validated.kit.csp?.directives?.['trusted-types']?.includes('sveltekit-trusted-url')
) {
throw new Error(
"The `csp.directives['trusted-types']` option must include 'sveltekit-trusted-url' when `serviceWorker.register` is true"
);
}
}

return validated;
}
24 changes: 20 additions & 4 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** @import { Validator } from './types.js' */

import process from 'node:process';
import colors from 'kleur';

/** @typedef {import('./types.js').Validator} Validator */
import { supportsTrustedTypes } from '../sync/utils.js';

const directives = object({
'child-src': string_array(),
Expand All @@ -28,8 +29,14 @@ const directives = object({
'navigate-to': string_array(),
'report-uri': string_array(),
'report-to': string_array(),
'require-trusted-types-for': string_array(),
'trusted-types': string_array(),
'require-trusted-types-for': validate(undefined, (input, keypath) => {
assert_trusted_types_supported(keypath);
return string_array()(input, keypath);
}),
'trusted-types': validate(undefined, (input, keypath) => {
assert_trusted_types_supported(keypath);
return string_array()(input, keypath);
}),
'upgrade-insecure-requests': boolean(false),
'require-sri-for': string_array(),
'block-all-mixed-content': boolean(false),
Expand Down Expand Up @@ -485,4 +492,13 @@ function assert_string(input, keypath) {
}
}

/** @param {string} keypath */
function assert_trusted_types_supported(keypath) {
if (!supportsTrustedTypes()) {
throw new Error(
`${keypath} is not supported by your version of Svelte. Please upgrade to Svelte 5.51.0 or later to use this directive.`
);
}
}

export default options;
8 changes: 7 additions & 1 deletion packages/kit/src/core/sync/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { import_peer } from '../../utils/import.js';
/** @type {{ VERSION: string }} */
const { VERSION } = await import_peer('svelte/compiler');

const [MAJOR, MINOR] = VERSION.split('.').map(Number);

/** @type {Map<string, string>} */
const previous_contents = new Map();

Expand Down Expand Up @@ -74,5 +76,9 @@ export function dedent(strings, ...values) {
}

export function isSvelte5Plus() {
return Number(VERSION[0]) >= 5;
return MAJOR >= 5;
}

export function supportsTrustedTypes() {
return (MAJOR === 5 && MINOR >= 51) || MAJOR > 5;
}
8 changes: 7 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,14 @@ export async function render_response({
// we use an anonymous function instead of an arrow function to support
// older browsers (https://github.com/sveltejs/kit/pull/5417)
blocks.push(`if ('serviceWorker' in navigator) {
const script_url = '${prefixed('service-worker.js')}';
const policy = globalThis?.window?.trustedTypes?.createPolicy(
'sveltekit-trusted-url',
{ createScriptURL(url) { return url; } }
);
const sanitised = policy?.createScriptURL(script_url) ?? script_url;
addEventListener('load', function () {
navigator.serviceWorker.register('${prefixed('service-worker.js')}'${opts});
navigator.serviceWorker.register(sanitised${opts});
});
}`);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/test/apps/options-2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"check": "svelte-kit sync && tsc && svelte-check",
"test": "pnpm test:dev && pnpm test:build",
"test:dev": "DEV=true playwright test",
"test:build": "playwright test"
"test:build": "playwright test && REGISTER_SERVICE_WORKER=true playwright test"
},
"devDependencies": {
"@sveltejs/adapter-node": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>this page will error when SvelteKit tries to register the service worker</p>
10 changes: 9 additions & 1 deletion packages/kit/test/apps/options-2/svelte.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import process from 'node:process';

/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
csp: {
directives: {
'require-trusted-types-for': ['script'],
'trusted-types': ['svelte-trusted-html', 'sveltekit-trusted-url']
}
},
paths: {
base: '/basepath',
relative: true
},
serviceWorker: {
register: false
register: !!process.env.REGISTER_SERVICE_WORKER
},
env: {
dir: '../../env'
Expand Down
54 changes: 54 additions & 0 deletions packages/kit/test/apps/options-2/test/service-worker.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { expect } from '@playwright/test';
import { test } from '../../../utils.js';

test.skip(({ javaScriptEnabled }) => !javaScriptEnabled || !process.env.REGISTER_SERVICE_WORKER);

test('import proxy /basepath/service-worker.js', async ({ request }) => {
test.skip(!process.env.DEV);

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const response = await request.get('/basepath/service-worker.js');
const content = await response.text();
expect(content).toEqual(
`import '${path.join('/basepath', '/@fs', __dirname, '../src/service-worker.js')}';`
);
});

test('build /basepath/service-worker.js', async ({ baseURL, request }) => {
test.skip(!!process.env.DEV);

const response = await request.get('/basepath/service-worker.js');
const content = await response.text();

const fn = new Function('self', 'location', content);

const self = {
addEventListener: () => {},
base: null,
build: null
};

const pathname = '/basepath/service-worker.js';

fn(self, {
href: baseURL + pathname,
pathname
});

expect(self.base).toBe('/basepath');
expect(self.build?.[0]).toMatch(/\/basepath\/_app\/immutable\/bundle\.[\w-]+\.js/);
expect(self.image_src).toMatch(/\/basepath\/_app\/immutable\/assets\/image\.[\w-]+\.jpg/);
});

test('works with CSP require-trusted-types-for', async ({ page }) => {
const errors = [];
page.on('pageerror', (err) => {
errors.push(err.message);
});

await page.goto('/basepath/csp-trusted-types');
expect(errors.length).toEqual(0);
});
67 changes: 14 additions & 53 deletions packages/kit/test/apps/options-2/test/test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { expect } from '@playwright/test';
import { test } from '../../../utils.js';

/** @typedef {import('@playwright/test').Response} Response */
test.skip(() => !!process.env.REGISTER_SERVICE_WORKER);

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

Expand Down Expand Up @@ -107,59 +105,22 @@ test.describe('paths', () => {
});

test.describe('trailing slash', () => {
if (!process.env.DEV) {
test('trailing slash server prerendered without server load', async ({
page,
clicknav,
javaScriptEnabled
}) => {
if (!javaScriptEnabled) return;

await page.goto('/basepath/trailing-slash-server');

await clicknav('a[href="/basepath/trailing-slash-server/prerender"]');
expect(await page.textContent('h2')).toBe('/basepath/trailing-slash-server/prerender/');
});
}
test('trailing slash server prerendered without server load', async ({
page,
clicknav,
javaScriptEnabled
}) => {
test.skip(!javaScriptEnabled || !process.env.DEV);

await page.goto('/basepath/trailing-slash-server');

await clicknav('a[href="/basepath/trailing-slash-server/prerender"]');
expect(await page.textContent('h2')).toBe('/basepath/trailing-slash-server/prerender/');
});
});

test.describe('Service worker', () => {
if (process.env.DEV) {
test('import proxy /basepath/service-worker.js', async ({ request }) => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const response = await request.get('/basepath/service-worker.js');
const content = await response.text();
expect(content).toEqual(
`import '${path.join('/basepath', '/@fs', __dirname, '../src/service-worker.js')}';`
);
});

return;
}

test('build /basepath/service-worker.js', async ({ baseURL, request }) => {
const response = await request.get('/basepath/service-worker.js');
const content = await response.text();

const fn = new Function('self', 'location', content);

const self = {
addEventListener: () => {},
base: null,
build: null
};

const pathname = '/basepath/service-worker.js';

fn(self, {
href: baseURL + pathname,
pathname
});

expect(self.base).toBe('/basepath');
expect(self.build?.[0]).toMatch(/\/basepath\/_app\/immutable\/bundle\.[\w-]+\.js/);
expect(self.image_src).toMatch(/\/basepath\/_app\/immutable\/assets\/image\.[\w-]+\.jpg/);
});
test.skip(({ javaScriptEnabled }) => !javaScriptEnabled);

test('does not register /basepath/service-worker.js', async ({ page }) => {
await page.goto('/basepath');
Expand Down
13 changes: 0 additions & 13 deletions packages/kit/test/apps/options/source/pages/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
<script>
import { onMount } from 'svelte';
import { setup } from '../../../../setup.js';

setup();

// TODO: remove this when Svelte 5 addresses this issue https://github.com/sveltejs/svelte/issues/14438
onMount(() => {
// @ts-expect-error trustedTypes is a limited availability global
// see https://developer.mozilla.org/en-US/docs/Web/API/Window/trustedTypes
if (window.trustedTypes && trustedTypes.createPolicy) {
// @ts-expect-error
trustedTypes.createPolicy('default', {
createHTML: (/** @type {string} */ str) => str
});
}
});
</script>

<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>this page will error when Svelte tries to set the innerHTML without a trusted type</p>
3 changes: 2 additions & 1 deletion packages/kit/test/apps/options/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const config = {
csp: {
directives: {
'script-src': ['self'],
'require-trusted-types-for': ['script']
'require-trusted-types-for': ['script'],
'trusted-types': ['svelte-trusted-html']
}
},
files: {
Expand Down
12 changes: 12 additions & 0 deletions packages/kit/test/apps/options/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@
expect(hydratable_script_match).not.toBeNull();
expect(hydratable_script_match?.[1]).toBe(nonce);
});

test('require-trusted-types-for', async ({ page, javaScriptEnabled }) => {
test.skip(!javaScriptEnabled, 'trusted types only affects scripts');

const errors = [];
page.on('pageerror', (err) => {
errors.push(err.message);
});

await page.goto('/path-base/csp-trusted-types');
expect(errors.length).toEqual(0);
});
});

test.describe('Custom extensions', () => {
Expand Down Expand Up @@ -188,7 +200,7 @@
expect(requests).toEqual([]);
});

test('accounts for base path when running data-sveltekit-preload-code', async ({

Check warning on line 203 in packages/kit/test/apps/options/test/test.js

View workflow job for this annotation

GitHub Actions / test-kit (24, ubuntu-latest, chromium, beta)

flaky test: accounts for base path when running data-sveltekit-preload-code

retries: 2
page,
javaScriptEnabled
}) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/test/prerendering/basics/test/tests.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ test('prerenders a page in a (group)', () => {

test('injects relative service worker', () => {
const content = read('index.html');
expect(content).toMatch("navigator.serviceWorker.register('./service-worker.js'");
expect(content).toMatch("const script_url = './service-worker.js';");
});

test('define service worker variables', () => {
Expand Down
Loading
Loading