← marwandiallo.comlabs

The four CSP shapes

Almost every CSP I see in the wild is a variant of one of four shapes. Pick the right starting point and you save yourself the migration.

1. Fully open (default state, also 'unsafe-inline')

default-src 'self' 'unsafe-inline' 'unsafe-eval' https:;

What you get if you "added a CSP" without thinking. Inline scripts run, eval runs, any HTTPS host is trusted, no XSS protection beyond what the browser already gives you. Worse than no CSP in some ways because it gives a false sense of security.

When to use: never on production. Acceptable for 5 minutes during initial rollout while you measure with Content-Security-Policy-Report-Only.

2. Allowlist (the most common)

default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpoint;

What 90% of teams ship. Works fine until your marketing team adds a sixth analytics vendor and someone adds https: to shut up the violations. Then it slowly degenerates into shape #1.

Hidden risk: any allowlisted CDN that hosts JSONP endpoints (cdn.jsdelivr.net, ajax.googleapis.com historically) gives an attacker an XSS bypass. See the bypasses page.

When to use: mature server-rendered sites where you control every script you load. Not great for SPAs that pull in a lot of third-party JS at runtime.

3. Nonce-based (recommended for most)

default-src 'self';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM}';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpoint;

The server generates a fresh random nonce per request, includes it in the CSP header, and stamps it on every legitimate <script> tag. Inline scripts work (because they have the right nonce); injected scripts don't (because the attacker doesn't know it). 'strict-dynamic' means any script that runs with a valid nonce can pull in further scripts dynamically — no need to allowlist every CDN.

This is what marwandiallo.com runs. Generated fresh per request via Next.js middleware.

When to use: server-rendered apps (Next.js, Rails, Django, ASP.NET). You need request-time control over the response.

4. Hash-based / pure 'strict-dynamic'

default-src 'self';
script-src 'sha256-AbC123...' 'sha256-XyZ789...' 'strict-dynamic';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';

For static sites where you can't generate per-request nonces. You hash the SHA-256 of every legitimate inline script at build time and embed the digests in the policy. Brittle (every script change rotates the hash) but works on a CDN-only static site.

When to use: static / CDN-served sites where you can't run server middleware. Or when you have a small, controlled set of inline scripts that rarely change.

Picking the shape for you

  1. Server-rendered with a small number of routes? Nonce.
  2. Static site on a CDN? Hash, or move to a host that lets you set per-request headers.
  3. Existing app you can't rewrite? Start allowlist in report-only mode, watch the violations for a week, then switch to enforce. Plan migration to nonce as the next step.
  4. New app, no existing constraints? Nonce from day one.