You deploy a new feature, someone files a bug, and the root cause is that your CSP is blocking a legitimate script from a CDN you added last week. You add unsafe-inline to silence the console errors, and now your entire XSS protection is gone. CSP done wrong is worse than useless — here's how to do it right.
Content Security Policy is the most effective browser-side XSS mitigation available, and a strict policy with nonces or hashes can stop the vast majority of injection attacks even when your output encoding has a gap. The key is building the policy incrementally rather than writing it all at once and hoping for the best.
Start in Report-Only Mode
Before you touch a production policy, add a Content-Security-Policy-Report-Only header. This mirrors exactly what a real policy would block, but doesn't actually block anything — it just sends violation reports to an endpoint you control.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations
Run this in production for at least a week. Look at the violation reports. You will find third-party scripts, inline event handlers, and eval() calls you didn't know existed. Every violation is a potential bypass you would have opened up if you'd gone straight to enforcement.
Your violation endpoint just needs to accept POST requests with JSON. Log them to your existing observability stack and set up an alert for new source origins you haven't whitelisted.
Build the Policy Directive by Directive
Don't write a one-liner policy. Build it directive by directive based on what your app actually needs.
Select a value for each directive to see your policy take shape.
default-src 'self' is your starting point. It sets the fallback for any directive you don't explicitly define. Everything else is additive.
script-src is where most of the security value lives. The goal is to avoid unsafe-inline entirely. If your app has inline <script> tags or onclick attributes, you have two options: nonces or hashes.
A nonce looks like this in your HTML:
<script nonce="r4nd0mV4lu3">/* your code */</script>And in your CSP header:
script-src 'nonce-r4nd0mV4lu3' 'strict-dynamic'
'strict-dynamic' is important here — it propagates trust to scripts loaded by your trusted scripts, which means you don't have to whitelist every CDN your bundle loader touches.
style-src follows the same logic as script-src. Inline styles used by CSS-in-JS libraries (styled-components, Emotion) will trip you up here. Nonces work for <style> tags. If you're on a framework that injects styles at runtime, you may need unsafe-inline for styles — this is acceptable since CSS injection is lower risk than script injection, but don't carry that same compromise into script-src.
img-src should be 'self' data: https: for most apps. The data: allowance is necessary for base64-encoded images used by charting libraries and rich text editors.
connect-src controls fetch, XHR, and WebSocket connections. List every API domain your frontend talks to. Missing one here causes silent failures in production that are a nightmare to debug — the request just fails with no visible error to the user.
frame-ancestors is not the same as X-Frame-Options, but use it to replace that header. frame-ancestors 'none' prevents clickjacking.
Generating Nonces Correctly
The single most common mistake with nonce-based CSP is generating a weak or reused nonce. A nonce must be:
- Cryptographically random (not
Math.random()) - At least 128 bits (16 bytes)
- Base64-encoded
- Unique per HTTP response — not per session, per response
The unsafe-inline Trap in SPAs
Here's the edge case that catches almost every React or Angular developer: 'strict-dynamic' does not work in browsers that don't support it (IE11), but more practically, some bundlers inject inline scripts during the build that don't have nonces. Create React App and older Angular CLI setups are frequent offenders.
The fix is to hash those injected scripts rather than adding unsafe-inline. Run your app, collect the violation reports, and for any inline script you can't nonce (because it's injected by your build tool), compute its SHA-256 hash and add it to the policy:
script-src 'nonce-abc123' 'sha256-BASE64_OF_SCRIPT_CONTENT' 'strict-dynamic'
The hash must be of the exact script content including whitespace. One extra space breaks it. If your build tool regenerates that inline script with a different content hash on every build, you'll need to automate hash extraction as part of your CI pipeline.
CSP and Next.js
Next.js has a specific pattern for nonces because the App Router renders server-side. As of Next.js 13+, you set the nonce in a middleware file and pass it via headers. The next.config.js needs to be updated so the framework doesn't strip your nonce from injected scripts — specifically, you need to set dangerouslyAllowSVG and configure the nonce in the headers() function or via the middleware approach documented in their security headers guide.
The critical detail people miss: Next.js in production generates static HTML for pages using generateStaticParams. Static pages can't have per-request nonces because there's no server context at render time. For fully static pages, fall back to hashes or accept that script-src for those routes will need a different policy.
What a Strict Production Policy Looks Like
A reasonable starting point for a server-rendered app with a trusted CDN:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-GENERATED_PER_REQUEST' 'strict-dynamic';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.yourdomain.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
base-uri 'self' is easy to miss — it prevents attackers from injecting a <base> tag that redirects all relative URLs to an attacker-controlled domain. form-action 'self' stops exfiltration via form hijacking. Both are one-line additions with no downside.
Open the CSP Generator tool on EncryptCodec right now, paste in your domain and the third-party origins your app uses, and generate a baseline policy. Then run it in Report-Only mode against your staging environment before you touch production.
Frequently Asked Questions
Related posts
CORS Explained: The 5 Most Dangerous Misconfigurations and How to Fix Them
Understand how CORS works, why browsers enforce it, and the five most common misconfigurations that expose your API to cross-origin attacks.
Mar 29, 2026 · 7 min readPrototype Pollution in JavaScript: How __proto__ Can Break Your App
Learn how prototype pollution works in JavaScript, review real-world CVEs in lodash and jQuery, and implement defenses to prevent this class of vulnerability.
Mar 29, 2026 · 7 min read