Svelte 5 doesn’t make CSP hard, but it does force you to be honest about how your app renders, hydrates, and injects code. That’s a good thing.
If you’re building with Svelte 5 and runes, the CSP story is mostly about three things:
- avoiding inline script and style surprises
- handling nonces correctly for SSR
- not breaking hydration or third-party tooling
Runes themselves don’t need special CSP directives. $state, $derived, and $effect are compile-time language features. CSP doesn’t care that you used runes. CSP cares whether the generated output includes inline JavaScript, inline styles, eval-like behavior, or external resources from origins you didn’t allow.
This guide is the practical version: what to set, what breaks, and what to paste into your app.
The short answer
For a typical SvelteKit app on Svelte 5, you usually want:
object-src 'none'base-uri 'self'frame-ancestors 'none'form-action 'self'default-src 'self'- a strict
script-srcusing nonces - a controlled
style-src - explicit
connect-src,img-src,font-src, andframe-srcas needed
If you can use nonces for scripts, do it. If you can avoid 'unsafe-inline' in styles, do that too. In practice, many apps keep 'unsafe-inline' for styles longer than they should.
A solid baseline CSP for SvelteKit
This is a good starting point for a self-hosted SvelteKit app with SSR:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
A few opinions here:
- I like
default-src 'self'as the base. - I use
'strict-dynamic'when I trust my nonce-bearing bootstrap scripts. - I keep
object-src 'none'always. - I set
frame-ancestors 'none'unless I explicitly want embedding.
If you need ready-made policy patterns, https://csp-examples.com is useful for comparing shapes of policies.
What Svelte 5 runes change
Almost nothing at the CSP layer.
This rune-heavy component:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log('count changed', count);
});
function increment() {
count += 1;
}
</script>
<button onclick={increment}>Clicked {count} times</button>
<p>Doubled: {doubled}</p>
does not require special CSP treatment just because it uses runes.
What matters is:
- the generated client bundle is loaded safely
- any inline hydration bootstrapping is nonce-protected
- you don’t add unsafe third-party snippets without accounting for them
The common mistake is blaming runes for a CSP error that was actually caused by analytics, a consent banner, a dev tool, or an injected inline script in your app template.
SvelteKit nonce setup
If you’re serving SSR HTML, nonces are the cleanest route.
Generate a nonce per request, attach it to your CSP header, and make sure SvelteKit uses it for any inline scripts it emits.
A common server hook pattern looks like this:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
export const handle: Handle = async ({ event, resolve }) => {
const nonce = randomUUID();
event.locals.nonce = nonce;
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%csp_nonce%', nonce)
});
response.headers.set(
'content-security-policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; ')
);
return response;
};
Then in your app template:
<!-- src/app.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
Depending on your setup, SvelteKit can handle CSP nonces directly through framework support rather than manual placeholder replacement. Check the official docs for your exact version:
My advice: verify the final rendered HTML in production and confirm every inline script has the same nonce as your header. Don’t assume.
Report-Only first, always
Before enforcing, use Report-Only:
response.headers.set(
'content-security-policy-report-only',
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; ')
);
This catches the weird stuff: browser extensions aside, the real offenders are usually tag managers, A/B testing tools, and “temporary” inline snippets someone pasted six months ago.
Development vs production
Your production CSP should be strict. Your dev CSP can be looser.
Vite and hot module reload often need extra allowances in dev, especially ws: or wss: for HMR and sometimes less strict script handling.
A dev-only policy might look like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' ws: wss: http: https:;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
I would never ship that to production, but I also wouldn’t waste time trying to make local HMR perfectly strict unless there’s a compliance reason.
Third-party services: real-world policy example
Here’s a real policy shape from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YmE3YmM0NWUtNjgxNC00MDY2LTgyZDktMmNkMTc4N2RiYjhh' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
This is pretty representative of what happens once marketing gets involved:
- Google Tag Manager needs script and connect allowances
- Google Analytics expands
connect-src - Cookiebot adds script, style, frame, and connect sources
- websocket endpoints show up in
connect-src
If your Svelte app uses those same vendors, this is the kind of policy you end up with.
Copy-paste version for SvelteKit:
const csp = [
"default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com`,
"style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com",
"frame-src 'self' https://consentcdn.cookiebot.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; ');
Would I love to remove 'unsafe-inline' from style-src here? Yes. In real deployments, consent tooling and injected UI often make that annoying fast.
Common Svelte CSP breakages
1. Inline scripts in app.html
If you add something like this:
<script>
window.appTheme = localStorage.getItem('theme') || 'light';
</script>
your CSP will block it unless you use a nonce or hash.
Safer version with a nonce:
<script nonce="%csp_nonce%">
window.appTheme = localStorage.getItem('theme') || 'light';
</script>
But honestly, I try to keep custom inline scripts out of app.html unless they’re absolutely necessary.
2. Third-party snippets pasted into components
This is a classic footgun:
<svelte:head>
<script>
(function () {
console.log('tracking init');
})();
</script>
</svelte:head>
That inline script needs a nonce too, and depending on how it’s rendered, you may not get one automatically. Better option: move the code into a module you control and load it as part of your app bundle.
3. WebSocket failures in dev or realtime apps
If your app uses a live backend or HMR, you need ws: or explicit websocket origins in connect-src.
Example:
connect-src 'self' https://api.example.com wss://realtime.example.com
4. Style attribute and injected CSS issues
Some UI libraries or consent managers inject styles dynamically. That often pushes teams toward:
style-src 'self' 'unsafe-inline'
Not ideal, but common. If you can move styles into static CSS files or use nonce-based style tags, do it.
A stricter production template
If your app is mostly first-party code and you don’t have messy third-party embeds, I’d start here:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-src 'none';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requests
That’s a decent “grown-up app” policy.
Final sanity checklist
Before you call your Svelte 5 CSP done, check these:
- every SSR response gets a fresh nonce
script-srcincludes that nonce- any inline script in the rendered HTML actually has the nonce
- dev-only allowances are not shipped to production
connect-srccovers your APIs, analytics, and websocketsframe-srccovers consent or embedded widgets if needed- you’re not using
'unsafe-eval'in production - you’re not using
'unsafe-inline'for scripts in production
Svelte 5 runes don’t complicate CSP. Third-party scripts do. SSR does. Careless inline code does.
If you keep your app bundle first-party, use nonces properly, and add external origins one by one instead of dumping in wildcards, CSP with SvelteKit is pretty manageable. That’s the version I’d ship.