DField SolutionsLoading · Töltődik
Skip to content

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-endpoint

default-src 'none' · the deny-all baseline

We 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.

script-src · nonce + strict-dynamic, not allowlist

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.

style-src · nonce, not unsafe-inline

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.

img-src · explicit allowlist

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.

font-src · self plus the one CDN

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:`.

connect-src · the most-skipped, most-important line

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 and frame-ancestors

`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 and base-uri

`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' and media-src

`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.

worker-src and manifest-src

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.

upgrade-insecure-requests

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.

report-to · pair with the Reporting API

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"}]}

How we roll a CSP without breaking the site

  1. Ship `Content-Security-Policy-Report-Only` first · the browser reports violations, blocks nothing.
  2. Watch the report endpoint for a week. Fix legitimate inline scripts and styles to use the nonce. Add real third parties to allowlists.
  3. When report volume drops to a steady trickle of known noise, flip to enforce mode.
  4. Keep `Report-Only` next to it for the new directives you are tightening · running both in parallel is supported.

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.

ShareXLinkedIn#
Dezso Mezo
By

Dezso Mezo

Founder, DField Solutions

I've shipped production products from fintech to creator-tooling · for startups and enterprises, from Budapest to San Francisco.

Keep reading
RELATED PROJECTS
Let's talk

Would rather build together?

Let's talk about your project. 30 minutes, no strings.