The CSP we ship · with notes on why each directive is there
Most CSPs in the wild are either too loose to matter or so strict the site breaks. Here is the one we ship, annotated.
Most CSPs in the wild are either too loose to matter or so strict the site breaks. Here is the one we ship, annotated.
Most production CSPs we audit fall into two camps. The 'unsafe-inline everywhere' kind, which exists to silence the browser warning and blocks roughly nothing. And the 'I copy-pasted from a hardening guide' kind, which is so strict the site breaks on the first analytics tag. Neither is helpful.
Here is the CSP we ship. It is not the strictest possible. It is the strictest one that still lets a real product run with analytics, payments, embedded video and a CMS. Every directive has a note: what it blocks, why we kept it, what the wrong default would be.
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-{NONCE}' 'strict-dynamic' https:;
style-src 'self' 'nonce-{NONCE}';
img-src 'self' data: https://images.example.com https://www.gstatic.com;
font-src 'self' data: https://fonts.gstatic.com;
connect-src 'self' https://api.example.com https://*.sentry.io https://www.google-analytics.com;
frame-src 'self' https://js.stripe.com https://www.youtube-nocookie.com;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
object-src 'none';
media-src 'self' https://videos.example.com;
worker-src 'self' blob:;
manifest-src 'self';
upgrade-insecure-requests;
report-to csp-endpointWe start from zero. Anything we did not allow explicitly is blocked. The opposite default · `default-src 'self'` · sounds tighter than it is, because it implicitly allows fetch, image, font, frame and worker from your own origin without making you think about each surface. Starting from `'none'` forces every directive to be a deliberate decision.
Three things on this line: `'self'`, a per-request `'nonce-{NONCE}'`, and `'strict-dynamic'`. The nonce is generated server-side and stamped on every inline `<script>` we ship, then carried by `strict-dynamic` to whatever those scripts dynamically inject. The `https:` at the end is a fallback for browsers that ignore `strict-dynamic` (older Safari mostly) and is overridden by it where it matters.
Why not a host allowlist? Because allowlists for `script-src` have been broken for years · researchers showed JSONP endpoints and AngularJS-host bypasses make most allowlists useless. Nonces with `strict-dynamic` survive that.
If you see `'unsafe-inline'` or `'unsafe-eval'` in `script-src`, you do not have a CSP. You have a sticker that says CSP. Fix the underlying inline scripts and remove the directive.
Same shape as `script-src`: own origin plus a per-request nonce. The nonce-based approach lets us emit critical CSS inline (LCP win) without giving up the policy. Tailwind, CSS-in-JS, and the Next.js streaming style chunk all stamp the nonce when wired up correctly.
Images are a common smuggling surface · pixel trackers, beacon callbacks, exfil-via-GET. We allow the origin, our image CDN, and one third-party we trust (here Google's static assets, which a sign-in widget needs). `data:` is included because emoji and small inline icons use it; if you do not need it, drop it.
Self-hosted fonts win on performance and privacy. The single Google fonts entry is a concession when the design team says no. If you self-host all fonts, this line collapses to `font-src 'self' data:`.
This is what `fetch`, `XMLHttpRequest`, `EventSource`, WebSocket and beacon use. Skip it and you get exfil-via-`fetch` for free. We list the API origin, the error-reporting endpoint (Sentry), and the analytics endpoint. Anything else is a 'why is this site phoning that?' incident.
`frame-src` is who we let load inside our pages: Stripe checkout iframes, embedded YouTube without cookies. `frame-ancestors 'none'` is who can put us in a frame · nobody. That replaces `X-Frame-Options: DENY` and prevents clickjacking. The two directives sound similar and do opposite things.
`form-action 'self'` blocks the 'inject a form that posts elsewhere' class of attack. `base-uri 'self'` stops attackers who get a stray `<base href>` tag in from rewriting all relative URLs. Both are cheap, both close real classes of bug.
`object-src 'none'` kills `<object>` and `<embed>` · Flash is dead, but the surface still exists in browsers and is a known XSS lever. `media-src` covers `<video>` and `<audio>`; we allow our video CDN explicitly.
Service workers, web workers, shared workers, all live in `worker-src`. We allow `'self'` plus `blob:` because a couple of libs we use spin workers off `Blob` URLs. `manifest-src 'self'` covers the PWA manifest.
Quietly rewrites any leftover `http://` request to `https://`. Cheap insurance against a third-party SDK shipping an `http://` URL in a payload. If you also use HSTS (you should), this is a belt to that pair of suspenders.
We send violations to a `Report-To` (and `Reporting-Endpoints` for the newer header) endpoint. Without this you have no idea what your CSP is blocking; you only hear when a user emails support that something looks broken. Pipe the reports to a database, dedupe by directive plus blocked URI, and alert on novel ones.
Reporting-Endpoints: csp-endpoint="https://api.example.com/csp/report"
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://api.example.com/csp/report"}]}If your team objects to the rollout cost, run the report-only mode for two weeks and show the report log. The first analytics-tag exfil attempt or a `data:` URL trying to load JS makes the case.
A CSP is not a checkbox. It is a contract about what code runs on your origin. The version above is not the most paranoid one possible · it is the one a real product can keep on without false-positives drowning the team. The shape that survives is: `default-src 'none'`, nonce + `strict-dynamic` for scripts and styles, explicit allowlists for the network surfaces, and a real reporting endpoint behind it. Everything else is variation on those four moves.

Founder, DField Solutions
I've shipped production products from fintech to creator-tooling · for startups and enterprises, from Budapest to San Francisco.
Let's talk about your project. 30 minutes, no strings.