Content Security Policy: What It Is and How to Implement It
Imagine you sanitise every user input, escape all output, and still find injected scripts running on your site. That scenario exists because XSS has many vectors — third-party libraries, browser extensions, dependency supply-chain attacks — that bypass input sanitisation entirely. Content Security Policy (CSP) is the browser-enforced safety net that limits what those injected scripts can actually do.
What CSP Does
CSP is an HTTP response header (or <meta> tag) that declares a whitelist of trusted content sources per resource type. If the browser encounters a resource that violates the policy, it blocks it — silently, or with a violation report.
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
This policy says: load everything from the same origin by default; load scripts only from the same origin or cdn.example.com. A <script src="https://evil.com/steal.js"> would be blocked even if it had been injected into the page.
The Key Directives
default-src — the fallback for any resource type not explicitly listed.
script-src — controls JavaScript. The most critical directive. Accepts:
'self'— same origin'none'— nothing- A URL like
https://cdn.example.com 'nonce-<base64>'— allows a specific inline script with a matching nonce'strict-dynamic'— allows scripts loaded by trusted scripts (useful for script loaders)
style-src — CSS sources. Same values as script-src.
img-src — image sources.
connect-src — controls what URLs can be fetched via fetch, XHR, WebSocket.
frame-src / frame-ancestors — controls embedding. frame-ancestors 'none' is the modern equivalent of X-Frame-Options: DENY.
form-action — restricts where <form> elements can submit to.
Inline Scripts Are the Tricky Part
By default, a CSP with script-src 'self' blocks all inline scripts (<script>...</script>) and event handlers (onclick="..."). This breaks most existing applications immediately.
The naive escape hatch is 'unsafe-inline':
Content-Security-Policy: script-src 'self' 'unsafe-inline'
Don't do this. It defeats the primary XSS protection because it allows any injected inline script to execute.
The correct solution is nonces. The server generates a random value per request, includes it in the CSP header and on each legitimate inline script:
Content-Security-Policy: script-src 'nonce-r4nd0m4bc'
<script nonce="r4nd0m4bc">
// This executes — nonce matches
initApp()
</script>
An attacker can't know the nonce in advance (it changes every request), so injected scripts without the correct nonce are blocked.
Report-Only Mode: Deploy Without Breaking Things
The biggest barrier to CSP adoption is fear of breaking production. The Content-Security-Policy-Report-Only header solves this: violations are reported but not blocked.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations
Deploy in report-only mode, collect violations for a week, refine your policy, then switch to enforcement. This workflow makes CSP safe to adopt incrementally.
A Strict Modern Policy
The Google security team documented a CSP approach that works well with modern frameworks:
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
Key points:
'none'asdefault-srcdenies everything not explicitly listed'strict-dynamic'allows scripts loaded by nonce-trusted scripts (handles dynamic imports and script loaders)object-src 'none'blocks Flash and plugins (always do this)base-uri 'none'prevents attackers from hijacking relative URLs with<base>frame-ancestors 'none'prevents clickjacking
CSP and Next.js
Next.js supports CSP nonces through middleware:
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'`
const response = NextResponse.next()
response.headers.set('Content-Security-Policy', csp)
response.headers.set('x-nonce', nonce)
return response
}
Then pass the nonce to your <Script> components via headers().
What CSP Doesn't Prevent
CSP is a mitigation, not a silver bullet:
- It doesn't prevent XSS injection — it limits what injected code can do
'unsafe-eval'is often required by frameworks that useeval()— reduces effectiveness- Data exfiltration via
imgtags to attacker-controlled domains still works unlessimg-srcis locked down - CSP doesn't help if the attacker compromises a trusted CDN
Use CSP alongside — not instead of — output encoding, input validation, HttpOnly cookies, and SameSite policies.
Conclusion
Content Security Policy is one of the highest-ROI security headers you can add to a web application. It takes XSS from "full account takeover" to "limited damage." The key is to avoid 'unsafe-inline', use nonces for legitimate inline scripts, and deploy in report-only mode first to catch violations before you break production.
