Content Security Policy (CSP) is an HTTP header and browser security standard designed to restrict the origins from which scripts, stylesheets, fonts, images, and frames can be executed or loaded. Standardized under the W3C CSP Level 3 specification, a well-configured policy provides a formidable defense-in-depth boundary against Cross-Site Scripting (XSS), data injection, and malicious iframe clickjacking.
However, implementing strict CSP in modern full-stack web applications (Next.js, Remix, Vite/React) frequently leads to broken third-party scripts, blocked analytics tags, hydration errors, and difficult-to-trace browser console warnings.
This production guide walks you through building a strict CSP policy, implementing dynamic cryptographic nonces, leveraging Content-Security-Policy-Report-Only, and systematically debugging policy violations.
1. Core Anatomy of CSP Directives
A CSP header consists of a semicolon-delimited list of directives that govern specific resource types:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0m123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
Essential Directives Explained
| Directive | Purpose | Recommended Strict Baseline |
|---|---|---|
default-src |
Fallback for all unlisted resource directives | 'self' |
script-src |
Controls executable JavaScript origins and inline blocks | 'self' 'nonce-{RANDOM}' 'strict-dynamic' |
style-src |
Controls CSS stylesheets and inline <style> tags |
'self' 'unsafe-inline' (or hashed styles) |
img-src |
Dictates valid image sources (SVGs, CDNs, data URIs) | 'self' data: https: |
font-src |
Restricts web font origins (Google Fonts, CDNs) | 'self' https://fonts.gstatic.com |
connect-src |
Restricts fetch(), XMLHttpRequest, WebSockets, EventSource |
'self' https://api.yourdomain.com |
object-src |
Restricts Flash, Java applets, and <embed> objects |
'none' |
base-uri |
Prevents malicious <base href="..."> injection |
'self' |
form-action |
Controls where <form> submissions can post |
'self' |
frame-ancestors |
Replaces X-Frame-Options to prevent Clickjacking |
'none' (or 'self') |
2. Eliminating 'unsafe-inline' with Cryptographic Nonces
Using script-src 'unsafe-inline' completely neutralizes CSP's XSS protection because any injected <script>alert(1)</script> payload will execute freely. The modern industry standard is Nonce-based CSP.
How Nonces Work
- The server generates a cryptographically random Base64 string for every HTTP response.
- The server injects the nonce into the
Content-Security-Policyheader:script-src 'nonce-{RANDOM}'. - The server adds the matching
nonceattribute to legitimate inline scripts:<script nonce="{RANDOM}">...</script>. - Any script tag injected by an attacker lacks the secret per-request nonce and is blocked by the browser.
Implementing Dynamic Nonces in Next.js (App Router)
In Next.js, generate the nonce in middleware.ts and attach it to both the outgoing request headers and the response headers:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Generate 128-bit random nonce
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim();
// Pass nonce to request headers so Server Components can read it
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', cspHeader);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Set the CSP header on the client response
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}
3. Top 5 CSP Violations & How to Fix Them
1. Refused to execute inline script because it violates...
- Cause: An inline
<script>tag or inline event handler (e.g.onclick="doSomething()") is present without a validnonceorsha256hash. - Fix: Remove inline handlers (
addEventListenerin external JS) or addnonce={nonce}to the<script>tag. Alternatively, compute the SHA-256 hash of the exact script content and add'sha256-abc...'toscript-src.
2. Refused to evaluate a string as JavaScript (unsafe-eval)
- Cause: A dependency uses
eval(),new Function(), orsetTimeout("string", 100). Common in legacy templating engines or Webpack source maps. - Fix: Avoid adding
'unsafe-eval'in production whenever possible. In dev environments, condition'unsafe-eval'onNODE_ENV === 'development'.
3. Refused to connect to 'https://analytics.example.com' because it violates connect-src
- Cause: An API request via
fetchoraxioscontacted a domain not explicitly allowed inconnect-src. - Fix: Add the exact endpoint origin or wildcard (e.g.,
connect-src 'self' https://*.sentry.io https://api.stripe.com;) to your policy.
4. Refused to load the image 'data:image/svg+xml;...'
- Cause: Inline SVG or Base64 images are blocked when
img-srcdoes not allowdata:orblob:. - Fix: Update
img-srcto:img-src 'self' data: blob: https:;.
5. Refused to frame 'https://...' because an ancestor violates frame-ancestors
- Cause: The embedded iframe or embedding parent domain is blocked.
- Fix: If your application must be embedded in an authorized third-party dashboard, configure:
frame-ancestors 'self' https://app.trusted-partner.com;.
4. Safe Deployment: Using Content-Security-Policy-Report-Only
Never deploy a strict CSP directly into production without testing against live traffic. Use Content-Security-Policy-Report-Only alongside a reporting URI:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-xyz'; report-uri /api/csp-report; report-to csp-endpoint;
In Report-Only mode:
- The browser does not block any resources.
- The browser logs warnings to the developer console.
- The browser transmits JSON violation payloads to your reporting endpoint so you can discover hidden third-party dependencies before enforcing the policy.
Frequently Asked Questions
Can I set Content Security Policy in a <meta> HTML tag?
Yes, using <meta http-equiv="Content-Security-Policy" content="...">. However, meta tags do not support frame-ancestors, report-uri, or sandbox. For full protection, set CSP via HTTP response headers at the server or edge proxy level.
What is the purpose of 'strict-dynamic' in CSP Level 3?
'strict-dynamic' tells modern browsers that any script authorized via a valid nonce or hash is trusted to load and execute downstream child scripts dynamically. This drastically simplifies third-party script loading (like Google Tag Manager or Stripe.js) without needing to allowlist dozens of dynamic CDN domains.
How do I validate and generate CSP headers visually?
Use the CSP Builder & Validator on DevFlow to assemble, test, and export production-ready CSP rules tailored for Next.js, Nginx, Apache, and Cloudflare.