Skip to main content

SvelteKit Advanced

Content Security Policy

Configure csp

SvelteKit writes the Content-Security-Policy header when you set its csp option. In SvelteKit 3 that option goes to the sveltekit() plugin in vite.config.ts. SvelteKit adds nonces or hashes to its own inline scripts. Add the hosts c15t and your vendors need to the directives:

vite.config.ts (partial)
export default defineConfig({
	plugins: [
		sveltekit({
			csp: {
				mode: 'auto',
				directives: {
					'connect-src': ['self', 'https://your-project.inth.app'],
					'frame-src': ['https://www.youtube-nocookie.com'],
					'script-src': ['self', 'https://<vendor-script-host>'],
				},
			},
		}),
	],
});

On SvelteKit 2, set the same csp object as kit.csp in svelte.config.js.

Replace https://your-project.inth.app with the backend URL from your Inth project, and list each vendor's script host.

SvelteKit does not pass the nonce it generates to your components, so the provider's nonce prop cannot reuse it. Allow vendor hosts in script-src instead. Host sources still apply next to SvelteKit's nonces, as long as the list has no 'strict-dynamic'.

The <link rel="modulepreload"> tag that c15tHandle adds for the script loader and network blocker chunk takes the nonce SvelteKit put on its own scripts, unless the provider has a nonce of its own. The chunk is on your own origin, so 'self' covers it too.

Allow c15t's style elements

c15t's components add their rules as <style> elements. On a server-rendered page, c15tHandle writes the banner's rules into the HTML <head> with the provider's nonce, or else the nonce SvelteKit put on its own scripts, so a nonce-based policy admits them. A prerendered page under mode: 'auto' gets hashes instead of nonces, so there is no nonce for c15tHandle to copy.

The standard and IAB dialogs and other components that first render in the browser use the provider's nonce, or inherit the nonce from an existing c15t style or a page script. Your style-src must allow that nonce.

For a prerendered page, or when your policy does not allow those nonces, set styles={false} on ConsentRoot and import @c15t/svelte/styles.css in your root layout. With IAB TCF, also import @c15t/svelte/iab/styles.css after it. 'self' covers those stylesheets.

Allow the generated theme

The <style id="c15t-theme"> element from customize is inline CSS that you render with {@html}. SvelteKit's csp option adds no nonce or hash to it, so a style-src without 'unsafe-inline' blocks it and the banner falls back to the stock colors. Either add the SHA-256 hash of the element's text to style-src, which stays fixed while the theme does not change, or switch to a stylesheet, which 'self' covers. Browsers ignore 'unsafe-inline' in a list that also has a nonce or hash, so adding it does not help under mode: 'nonce' or 'auto'.

Style attributes in server HTML

In the browser, Svelte sets style values through the DOM, which a policy does not block. Server-rendered HTML is different. A style attribute in the HTML is inline style, and style-src without 'unsafe-inline' blocks it. c15t renders style attributes for theme slot style values and for the position of ConsentDialogTrigger. Svelte does not set them again during hydration, so a blocked value stays unapplied.

Use class names in theme slots instead of style objects, or allow style attributes with a separate style-src-attr 'unsafe-inline' directive.

Server requests

loadConsent, c15tHandle and the manifest route call the backend from your server, which the browser's policy does not govern. Only the browser's own requests, such as saving a choice, need the backend in connect-src. With the consent proxy, those go to your own origin and 'self' covers them.

What c15t adds to the page

A Content Security Policy has to allow each thing c15t or your vendors put on the page:

c15t doesDirectiveAllow
Requests the policy from /init and saves choices to /subjectsconnect-srcYour backend's origin. 'self' covers a same-origin route such as /api/c15t.
Adds a <script> element for each allowed entry in scriptsscript-srcEach vendor's script host, or the provider's nonce.
Lets vendor scripts load more scripts, pixels and requestsscript-src, img-src, connect-srcThe hosts each vendor documents.
Sets src on gated iframes, or mounts ConsentGate childrenframe-srcEach embed's host, such as https://www.youtube-nocookie.com.
Adds its rules to <head> as <style> elementsstyle-srcThe provider's nonce. With styles={false} and an imported @c15t/svelte/styles.css, 'self'.
Fetches the Global Vendor List under an IAB policyconnect-srcThe vendor list host the policy names, when it is not your own origin.

The dev tools panel attaches a stylesheet to its shadow root. Keep the panel out of production builds, as its page describes, and a production policy does not need to allow it.

Stamp a nonce on vendor scripts

If your server creates a nonce for each response and puts it in the Content-Security-Policy header as 'nonce-<value>', pass the same value to the provider:

<!-- Svelte: src/App.svelte -->
<ConsentProvider mode={manifest()} {scripts} nonce={cspNonce}>

<!-- SvelteKit: src/routes/+layout.svelte -->
<ConsentRoot state={data.consent} {scripts} nonce={cspNonce}>

The script loader then sets that nonce on every <script> it adds, for both src and inline textContent scripts. A nonce on one scripts entry wins for that script. The components put it on the <style> elements they add. The provider reads nonce once, when it is created.

The nonce covers only the elements c15t creates. A script that a vendor's script loads by itself does not get it; allow that host in script-src, or add 'strict-dynamic' so scripts loaded by a trusted script are trusted too. With 'strict-dynamic', browsers ignore host allowlists in script-src, so every script on the page needs the nonce or a hash.

Verify the policy

Enforce the policy, not report-only, and open the console with site data cleared:

  1. The banner renders with its styles, and no Refused to message mentions a c15t element, a <style data-c15t-styles> element or @c15t/svelte/styles.css. Open the preference dialog and check again.
  2. Allow a category. Its vendor scripts load, and none are refused. A refused script from a vendor's own host means that host is missing from script-src.
  3. Save a choice. The request to /subjects succeeds; a connect-src violation means the backend's origin is missing.
  4. Allow the category of a gated embed. It loads; a frame-src violation names the host to add.