What is CSP?
Content Security Policy is an HTTP response header that tells the browser which content sources are trusted. It is the most effective defence-in-depth control against Cross-Site Scripting (XSS) attacks after input sanitisation.
Without CSP, a single XSS vulnerability lets an attacker inject arbitrary scripts. With a strict CSP, even injected scripts are blocked at the browser level.
The Anatomy of a CSP Header
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAndOm123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
Key Directives
| Directive | Purpose |
|---|---|
default-src | Fallback for all resource types |
script-src | JavaScript sources |
style-src | CSS sources |
img-src | Image sources |
connect-src | Fetch / XHR / WebSocket destinations |
frame-ancestors | Replaces X-Frame-Options |
base-uri | Restricts <base> tag hijacking |
form-action | Where forms can submit to |
The Nonce-Based Approach (Strict CSP)
Using 'unsafe-inline' for scripts completely undermines the XSS protection. The right approach is nonces — cryptographically random tokens generated per request.
Server-side (Node.js / Express example)
import crypto from 'crypto';
import helmet from 'helmet';
app.use((req, res, next) => {
// Generate a cryptographically random nonce per request
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use((req, res, next) => {
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
})(req, res, next);
});
Template (EJS / Handlebars)
<script nonce="<%= locals.cspNonce %>">
// This inline script is now allowed
console.log('Secure inline script');
</script>
Report-Only Mode: Safe Rollout
Never deploy a strict CSP straight to production. Use Report-Only mode first to catch violations without breaking anything.
Content-Security-Policy-Report-Only:
default-src 'self';
report-uri /csp-violation-report-endpoint
Collect reports for 1–2 weeks, triage violations, then flip to enforcement mode.
Testing Your CSP
Browser DevTools
Open the Console tab — CSP violations are logged in red with the exact blocked resource.
Online Scanners
- Mozilla Observatory:
observatory.mozilla.org - CSP Evaluator:
csp-evaluator.withgoogle.com - securityheaders.com
Command Line
# Quick header check with curl
curl -sI https://yourdomain.com | grep -i "content-security"
# Or use nikto for a broader header scan
nikto -h https://yourdomain.com
Common Pitfalls
'unsafe-inline'inscript-src— Negates almost all XSS protection. Use nonces.- Wildcard
*inscript-src— Allows any external script. Extremely dangerous. - Missing
frame-ancestors— Leaves you vulnerable to clickjacking. - Missing
base-uri—<base>tag injection can redirect all relative URLs. - Forgetting
form-action— Attackers can redirect form submissions to their server.
Takeaways
A strict CSP is not optional for serious web applications. Pair it with:
- Input validation and output encoding (never trust user input)
HttpOnlyandSecurecookie flagsX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin
Together these form a robust HTTP security header stack.