CSP (Content Security Policy) Setup for Websites
Content Security Policy — header telling browser where scripts, styles, fonts, images and other resources are allowed to load from. Properly configured CSP makes XSS attacks practically useless: even if attacker injects malicious script, browser blocks it.
Directive Anatomy
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com 'nonce-{RANDOM}';
style-src 'self' https://fonts.googleapis.com 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Key directives:
| Directive | Controls |
|---|---|
script-src |
Where JS scripts load from |
style-src |
Where CSS loads from |
img-src |
Image sources |
connect-src |
XHR, fetch, WebSocket |
frame-ancestors |
Who can embed page in iframe |
form-action |
Where forms submit |
Nonce-based Approach
'unsafe-inline' for scripts defeats CSP. Instead use nonce — random value generated on server for each request:
// PHP/Laravel
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'self' 'nonce-{$nonce}'");
// In template
<script nonce="{{ $nonce }}">
// this script passes validation
</script>
In Next.js via middleware:
// middleware.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export function middleware(request: Request) {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = `script-src 'self' 'nonce-${nonce}'; ...`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
Report-Only Mode
Before enforcing CSP, run in monitoring mode:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Browser sends JSON reports of violations without blocking content. Analysis over 1–2 weeks shows which sources need whitelist.
Example report:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src-elem",
"blocked-uri": "https://evil.com/payload.js",
"disposition": "report"
}
}
SPA and CDN Challenges
React/Vue/Angular apps often use eval() or dynamic script generation via webpack. This conflicts with CSP:
-
Webpack: use
devtool: 'source-map'instead ofeval, setupTrustedTypes -
Google Analytics / GTM: add
https://www.google-analytics.comandhttps://www.googletagmanager.comtoscript-srcandconnect-src -
Inline styles from JS libraries: use
style-srcwith'unsafe-inline'only if no alternative, or move to CSS classes
Nginx Configuration
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; ..." always;
For dynamic nonces better manage header at application level, not Nginx.
Monitoring Violations
Setup endpoint for CSP report collection or use third-party services: Report URI, Sentry (supports CSP reporting). Allows catching legitimate sources forgotten in policy and tracking real XSS attempts.
Common Mistakes
-
'unsafe-inline'and'unsafe-eval'inscript-src— defeats XSS protection - Wildcard
*indefault-src— same effect - Missing
frame-ancestors— site vulnerable to Clickjacking - Forgetting
wss://inconnect-srcwhen using WebSocket
Implementation Timeline
- Audit current resource sources: 2–4 hours
- Setup Report-Only + data collection: 1–2 weeks
- Transition to enforcement with debugging: 3–5 days







