How to Set Up Content Security Policy (CSP)
The most powerful security header - configured correctly
TL;DR
TL;DR (25 minutes)
Content Security Policy prevents XSS by controlling which resources can load. Start with Content-Security-Policy-Report-Only to test without breaking your site. Use default-src 'self' as your baseline, add nonces for inline scripts, and include third-party domains only where needed. Monitor violations with report-uri before switching to enforced mode.
Prerequisites
- Ability to set HTTP headers (via server config, middleware, or hosting platform)
- Understanding of what resources your app loads (scripts, styles, images, fonts)
- Access to browser DevTools for testing
- Optional: A reporting endpoint for CSP violations
What is Content Security Policy?
CSP is an HTTP header that tells browsers which sources of content are trusted. When a resource violates the policy, the browser blocks it and optionally reports the violation.
Why CSP Matters
Even if an attacker injects malicious code into your page (via XSS or compromised dependencies), CSP prevents it from executing if it doesn't match your policy. It's your last line of defense against script injection.
CSP Directive Reference
| Directive | Controls | Example |
|---|---|---|
| default-src | Fallback for all resource types | 'self' |
| script-src | JavaScript sources | 'self' 'nonce-abc' |
| style-src | CSS sources | 'self' 'unsafe-inline' |
| img-src | Image sources | 'self' data: https: |
| font-src | Font sources | 'self' https://fonts.gstatic.com |
| connect-src | XHR, WebSocket, fetch() URLs | 'self' https://api.example.com |
| frame-src | iframe sources | 'self' https://youtube.com |
| frame-ancestors | Who can embed this page | 'none' or 'self' |
| object-src | Flash, plugins | 'none' |
| base-uri | <base></base>tag URLs | 'self' |
| form-action | Form submission URLs | 'self' |
Step-by-Step Implementation
Start with Report-Only Mode
Use Content-Security-Policy-Report-Only to test without breaking your site:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
This logs violations to the console and your reporting endpoint without blocking anything.
Build Your Base Policy
Start strict and add exceptions as needed:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
Add Third-Party Resources
Identify and allow external resources your app needs:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' data: https: blob:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com https://analytics.example.com;
frame-src https://www.youtube.com https://player.vimeo.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Implement Nonces for Inline Scripts
Instead of using 'unsafe-inline', use nonces:
Generate a unique nonce for each request:
// Express.js middleware
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use((req, res, next) => {
const nonce = res.locals.nonce;
res.setHeader('Content-Security-Policy',
`default-src 'self'; ` +
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; ` +
`style-src 'self' 'nonce-${nonce}';`
);
next();
});
Add nonce to your inline scripts:
<!-- In your HTML template -->
<script nonce="<%= nonce %>">
// This inline script will execute
console.log('Authorized inline script');
</script>
<script>
// This will be BLOCKED - no nonce
console.log('Unauthorized script');
</script>
Use Hashes for Static Inline Scripts (Alternative)
For static inline scripts that don't change, use hashes:
# Generate hash of your inline script
echo -n "console.log('Hello');" | openssl dgst -sha256 -binary | openssl base64
# Output: 5jFwrAK0UV47oFbVg/iCCBbxD8X1w+QvoOUepu4C2YA=
Add the hash to your CSP:
Content-Security-Policy: script-src 'self' 'sha256-5jFwrAK0UV47oFbVg/iCCBbxD8X1w+QvoOUepu4C2YA=';
Set Up Violation Reporting
Create an endpoint to receive CSP violation reports:
// Express.js endpoint for CSP reports
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.log('CSP Violation:', {
blockedUri: report['blocked-uri'],
violatedDirective: report['violated-directive'],
documentUri: report['document-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number'],
});
// Send to your logging service
// await logService.log('csp-violation', report);
res.status(204).end();
});
Add report-uri to your CSP:
Content-Security-Policy: default-src 'self'; report-uri /csp-report;
Modern reporting with report-to:
# Headers
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"/csp-report"}]}
Content-Security-Policy: default-src 'self'; report-to csp-endpoint;
Switch to Enforced Mode
Once violations are resolved, remove -Report-Only from the header:
# Before (testing)
Content-Security-Policy-Report-Only: default-src 'self'; ...
# After (enforced)
Content-Security-Policy: default-src 'self'; ...
CSP Security Checklist
- Never use
'unsafe-inline'for script-src if possible - use nonces instead - Avoid
'unsafe-eval'unless absolutely required (some frameworks need it) - Always include
object-src 'none'to block Flash/plugins - Use
frame-ancestors 'none'or'self'to prevent clickjacking - Include
base-uri 'self'to prevent base tag hijacking - Test thoroughly in report-only mode before enforcing
- Monitor violation reports after deployment
Common CSP Configurations
Strict CSP for Modern Apps
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.yoursite.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
Next.js / React Application
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' wss: https:;
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
Note: 'unsafe-eval' is needed for development hot reloading. Consider removing in production.
Static Marketing Site
Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://www.google-analytics.com;
frame-src https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self' https://formsubmit.co;
frame-ancestors 'none';
upgrade-insecure-requests;
E-commerce with Payment Integration
Content-Security-Policy:
default-src 'self';
script-src 'self' https://js.stripe.com https://www.paypal.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https: blob:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.stripe.com https://api.yoursite.com;
frame-src https://js.stripe.com https://www.paypal.com;
object-src 'none';
base-uri 'self';
form-action 'self' https://api.stripe.com;
frame-ancestors 'none';
How to Verify It Worked
Method 1: Browser Console
- Open DevTools (F12)
- Go to Console tab
- Look for CSP violation errors (red text mentioning "Refused to...")
- Each violation shows what was blocked and why
Method 2: CSP Evaluator
Use Google's CSP Evaluator to analyze your policy for weaknesses.
Method 3: Security Headers Scanner
# Check your CSP header
curl -I https://yoursite.com | grep -i content-security-policy
Method 4: Test XSS is Blocked
Try injecting a script via URL parameters (if your app reflects user input):
# This should be blocked by CSP
https://yoursite.com/search?q=<script>alert('xss')</script>
Common Errors and Troubleshooting
Inline scripts blocked
- Solution A: Add nonces to inline scripts
- Solution B: Move inline scripts to external files
- Solution C: Use
'unsafe-inline'(weakens security)
Third-party scripts blocked
- Identify the domain: Check console for the blocked URL
- Add to script-src:
script-src 'self' https://cdn.example.com - Check subdomains: You might need
https://*.example.com
Styles not loading
- Inline styles: Add
'unsafe-inline'to style-src or use nonces - External stylesheets: Add the stylesheet domain to style-src
- CSS-in-JS: Many libraries require
'unsafe-inline'
Images not loading
- Base64 images: Add
data:to img-src - External images: Add
https:or specific domains - Blob URLs: Add
blob:to img-src
API calls blocked
- Fetch/XHR: Add API domain to connect-src
- WebSockets: Add
wss:and WebSocket URL to connect-src
Pro Tip: Iterative CSP Development
Start with the most permissive working policy, then gradually tighten it. Use report-only mode and violation logs to identify what needs to be allowed. Aim to remove 'unsafe-inline' and 'unsafe-eval' over time.
Frequently Asked Questions
What is Content Security Policy (CSP)?
CSP is an HTTP header that tells browsers which content sources are allowed on your page. It prevents XSS attacks by blocking unauthorized scripts, styles, images, and other resources from loading or executing.
Why does my CSP break my website?
CSP blocks content that doesn't match your policy. Common issues: inline scripts blocked (use nonces), third-party resources blocked (add their domains), or eval() calls blocked (add 'unsafe-eval'). Check browser console for specific violations.
Should I use 'unsafe-inline' in my CSP?
'unsafe-inline' significantly weakens XSS protection. Use nonces or hashes for inline scripts instead. For styles, 'unsafe-inline' is often needed for CSS-in-JS libraries, but migrate to external stylesheets when possible.
What is a CSP nonce and how does it work?
A nonce is a random, one-time value generated for each request. Add it to your CSP header (script-src 'nonce-abc123') and to each authorized inline script (nonce="abc123"). Only scripts with matching nonces execute.
What is the difference between report-only and enforced mode?
Content-Security-Policy blocks violations. Content-Security-Policy-Report-Only only reports them without blocking. Start with report-only to identify issues, then switch to enforced once your policy is correct.
Test Your CSP Configuration
Scan your site to check if CSP is configured correctly and identify potential weaknesses.
Start Free Scan