Content Security Policy: A Practical Guide to CSP Headers
A Content Security Policy tells the browser which scripts, styles, and connections your page may use, turning many XSS bugs into blocked requests. The directives that matter, nonce-based strict CSP, Report-Only rollout, Next.js specifics, and mistakes that make CSP useless.
Escaping output prevents cross-site scripting. A Content Security Policy (CSP) is what saves you when escaping fails somewhere — a dangerouslySetInnerHTML you missed, a vulnerable dependency, a Markdown renderer that lets HTML through. It's a response header that tells the browser which sources of script, style, images, and network connections are allowed, and blocks the rest. (What Is XSS?.)
What it looks like
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Each directive governs a resource type; each lists allowed sources:
| Directive | Controls |
|---|---|
default-src |
Fallback for most fetch directives not specified |
script-src |
JavaScript — the directive that matters most |
style-src |
CSS |
img-src, font-src, media-src |
Images, fonts, audio/video |
connect-src |
fetch, XHR, WebSockets, EventSource |
frame-src |
What you may embed in iframes |
frame-ancestors |
Who may embed you (clickjacking protection) |
object-src |
Plugins — set to 'none' |
base-uri |
Restricts <base> — set to 'none' or 'self' |
form-action |
Where forms may submit |
Source values include 'self' (same origin), specific origins (https://js.stripe.com), 'none', 'unsafe-inline', 'unsafe-eval', nonces, and hashes.
Why allowlists alone are weak
The intuitive CSP is a list of trusted domains:
script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com
Research has repeatedly shown that host allowlists are frequently bypassable: large CDNs and script hosts serve content that can be abused (old library versions, JSONP endpoints, user-uploaded files). And allowing 'unsafe-inline' for scripts disables most of CSP's XSS protection outright.
Strict CSP: nonces (or hashes) plus strict-dynamic
The recommended approach is a strict CSP:
Content-Security-Policy:
script-src 'nonce-R4nd0mV4lu3' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
'nonce-…'— a random value generated per response. Only<script nonce="R4nd0mV4lu3">tags run. An injected<script>without the nonce is blocked.'strict-dynamic'— scripts loaded by a trusted (nonced) script are also trusted, so bundlers and tag loaders keep working without listing every domain.https:and'unsafe-inline'are fallbacks ignored by modern browsers when a nonce andstrict-dynamicare present; they only keep very old browsers working.
For static pages that can't generate per-request nonces, use hashes of your inline scripts instead ('sha256-…').
Important: a nonce must be unpredictable and unique per response. A nonce baked into a cached static page protects nothing.
Rolling it out without breaking production
- Start in report-only mode. The browser reports violations but blocks nothing:
(Content-Security-Policy-Report-Only: script-src 'nonce-…' 'strict-dynamic'; object-src 'none'; base-uri 'none'; report-to csp Reporting-Endpoints: csp="https://example.com/csp-reports"report-uriis the older reporting directive; include both if you need broad browser coverage.) - Collect reports for a week or two — browser extensions generate a lot of noise; look for patterns from your own pages.
- Fix the sources: move inline event handlers (
onclick="…") into script files, add nonces to legitimate inline scripts, and adjustconnect-src/img-srcfor real third parties. - Enforce, keeping reporting on to catch regressions.
Framework specifics
Next.js supports nonce-based CSP: generate a nonce per request in the request interceptor (the file formerly known as middleware, now proxy.ts in Next.js 16), set the header, and Next.js applies the nonce to its own scripts. Using nonces forces those pages to render dynamically — you can't statically cache a page whose nonce must change per request. (What Is Next.js? and Self-Hosting Next.js Without Vercel.)
SPAs served as static files (Vite builds) usually use hash-based CSP or a policy set at the CDN/proxy, since there's no server to mint nonces per request.
Third-party scripts — analytics, chat widgets, payment SDKs, tag managers — each need their script, connect, frame, and image sources. Payment providers typically document the exact CSP entries required. Tag managers that inject arbitrary scripts sit in tension with a strict CSP by design; keep them to a minimum. (Analytics for Beginners.)
A reasonable starting policy
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example-provider.com;
frame-ancestors 'none';
object-src 'none';
base-uri 'none';
form-action 'self';
upgrade-insecure-requests
Tune img-src, connect-src, and frame-src to what you actually use.
Mistakes that make CSP useless
script-src 'unsafe-inline'without a nonce or hash — inline injection works again.'unsafe-eval'kept because one library needs it — find an alternative if you can.- Wildcards like
https:or*.cdnprovider.comas the primary control. - Static nonces — the same value on every response.
- Setting CSP via
<meta>and expectingframe-ancestorsor reporting to work — they don't in meta tags; use the header. - Forgetting other headers: pair CSP with
X-Content-Type-Options: nosniff,Referrer-Policy, and HTTPS with HSTS. (What Is HTTPS?.)
Checking
- Browser DevTools console shows each violation.
- Google's CSP Evaluator flags weak policies.
- A test that loads key pages and asserts no CSP violations catches regressions in CI.
CSP is defence in depth — it doesn't replace escaping, sanitisation, or HttpOnly session cookies. It limits how bad a single mistake can be.
EasySpawn runs your app as a real server behind HTTPS on your own domain, so you can set per-request CSP headers with fresh nonces — not just a static policy in a meta tag. See how it works or join the waitlist.
Related: What Is XSS? · CSRF Explained · A Security Checklist for Vibe-Coded Apps
Keep reading
Validating Input With Zod: One Schema for Forms, APIs, and Types
Every trust boundary — request bodies, query strings, webhooks, environment variables, AI output — needs runtime validation TypeScript can't provide. Using Zod schemas at each boundary, sharing them between client and server, stripping unknown keys, and useful errors.
Soft Deletes and Audit Logs: Keeping History Without Making a Mess
Deleting rows is irreversible; hiding them has costs too. When to use soft deletes, how to implement them without leaking 'deleted' data (partial indexes, unique constraints, views, RLS), the privacy tension with erasure requests, and how to build an audit log with triggers or application events.