// LOADING
// LOADING
// LOADING_ARTICLE
A Content Security Policy (CSP) is an HTTP response header that instructs the browser to only load resources from sources you explicitly allow. Its primary purpose is defense-in-depth against cross-site scripting: even if an attacker injects a <script> tag into your page, the browser refuses to execute it unless the script's source is on your whitelist.
CSP is not a replacement for input sanitization or output encoding — it is an additional layer that contains damage when something slips through.
The Content-Security-Policy header contains a list of directives, each controlling a resource type:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
Key directives:
| Directive | Controls |
|---|---|
| default-src | Fallback for all resource types not explicitly specified |
| script-src | JavaScript execution |
| style-src | CSS stylesheets |
| img-src | Images |
| connect-src | Fetch, XHR, WebSocket |
| frame-ancestors | Which pages can embed this page in an iframe |
| form-action | Where forms can submit |
When the browser encounters a resource that violates the policy, it blocks the request and (optionally) sends a report to your reporting endpoint.
unsafe-inline'unsafe-inline' in script-src allows inline <script> tags and javascript: URLs. This defeats CSP's primary purpose — it is the exact attack vector CSP is designed to block. Any policy that includes 'unsafe-inline' in script-src provides minimal XSS protection.
The same applies to 'unsafe-eval', which enables eval(), new Function(), and setTimeout(string).
The challenge with Next.js is that it historically relied on inline scripts for hydration data. Strict CSP requires a workaround.
A nonce is a cryptographically random value generated per request. The server embeds it in the CSP header and in the nonce attribute of every legitimate inline script. The browser executes only inline scripts whose nonce matches the header.
Content-Security-Policy: script-src 'nonce-rAnd0mStr1ng' 'strict-dynamic';
html<script nonce="rAnd0mStr1ng">/* Next.js hydration code */</script>
An attacker injecting <script>alert(1)</script> cannot know the nonce (it changes every request), so the browser blocks their script.
'strict-dynamic' is a companion directive that trusts scripts loaded by nonce-authorized scripts — this is necessary because Next.js loads additional script chunks dynamically at runtime.
ts// middleware.ts import { NextRequest, NextResponse } from 'next/server'; import { randomBytes } from 'crypto'; export function middleware(request: NextRequest) { const nonce = randomBytes(16).toString('base64'); const csp = [ `default-src 'self'`, `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, `style-src 'self' 'unsafe-inline'`, // many CSS-in-JS solutions need this `img-src 'self' blob: data: https:`, `font-src 'self'`, `connect-src 'self'`, `frame-ancestors 'none'`, `form-action 'self'`, `base-uri 'self'`, `object-src 'none'`, ].join('; '); const response = NextResponse.next(); response.headers.set('Content-Security-Policy', csp); // Pass nonce to the app via a request header request.headers.set('x-nonce', nonce); return response; } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico).*)', ], };
Then read the nonce in your root layout to pass it to <Script> components:
tsx// app/layout.tsx import { headers } from 'next/headers'; import Script from 'next/script'; export default function RootLayout({ children }: { children: React.ReactNode }) { const nonce = headers().get('x-nonce') ?? ''; return ( <html> <head> <Script nonce={nonce} strategy="beforeInteractive" src="/lib/analytics.js" /> </head> <body>{children}</body> </html> ); }
CSP violations that block resources will break your site if the policy is wrong. Use Content-Security-Policy-Report-Only to audit first:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-...' 'strict-dynamic';
report-uri /api/csp-report
In report-only mode, the browser logs violations but does not block them. Collect these reports for a week in production, fix any legitimate sources you missed, then switch to enforcement mode.
ts// app/api/csp-report/route.ts import { NextRequest } from 'next/server'; export async function POST(req: NextRequest) { const report = await req.json(); // Log to your observability stack console.error('CSP violation', JSON.stringify(report)); return new Response(null, { status: 204 }); }
frame-ancestors 'none' prevents any page from embedding yours in an <iframe>. This blocks clickjacking attacks, where an attacker overlays a transparent iframe of your site over a deceptive page to trick users into clicking buttons. This directive supersedes the older X-Frame-Options header — but set both for broad compatibility:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
While configuring CSP, set these companion headers in the same middleware:
tsresponse.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
X-Content-Type-Options: nosniff prevents MIME-type sniffing, which some browsers use in ways that can enable attacks.
CSP is the last line of defense against XSS. The first lines are sanitizing user input, escaping output, and avoiding dangerouslySetInnerHTML with unsanitized content. See preventing XSS in React and Next.js for those controls. A strict CSP header means that if sanitization fails — and it occasionally does — the browser still refuses to execute the injected code.
For a full picture of the web attack surface, common web security threats and how to prevent them is a useful companion. If you are also hardening your API layer, see securing REST APIs and rate limiting your Next.js APIs.
'unsafe-inline' and 'unsafe-eval' in script-src.'strict-dynamic' in Next.js — it is compatible with the framework's inline hydration scripts.Report-Only mode first; graduate to enforcement once violations are clean.frame-ancestors 'none' to block clickjacking.X-Content-Type-Options, Referrer-Policy) in the same middleware pass.Security configuration like this is worth reviewing in depth before deploying to production. Browse projects to see how these patterns are applied in practice, and check the about page to learn more about the author's background.