Content Security Policy (CSP): Implementation Guide
How do I implement Content Security Policy (CSP)?
TL;DR
- Bottom line: Deploy a strict nonce-based CSP with
strict-dynamicto prevent inline script injection (XSS) -- start in report-only mode, then enforce after validating no breakage. - Key tool/command:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; - Watch out for: Using
'unsafe-inline'inscript-srcdefeats CSP's entire XSS protection -- use nonces or hashes instead. - Works with: All modern browsers. CSP Level 3 (
strict-dynamic) supported in Chrome 59+, Firefox 58+, Safari 15.4+, Edge 79+.
Constraints
- ALWAYS start with
Content-Security-Policy-Report-Onlybefore enforcing -- deploying CSP in enforcement mode without testing WILL break site functionality - NEVER use
'unsafe-inline'forscript-srcin production -- it defeats CSP's primary XSS protection - NEVER use wildcard (
*) as a source forscript-srcordefault-src-- it allows loading scripts from any origin - Nonces MUST be cryptographically random (at least 128 bits) and regenerated on every page load -- reusing nonces defeats their purpose
- CSP is defense-in-depth, NOT a replacement for output encoding -- always combine CSP with proper input validation and output encoding
frame-ancestorsdirective CANNOT be set via<meta>tag -- it must be sent as an HTTP header
Quick Reference
CSP Directives by Category
| # | Directive | Category | Controls | Default Fallback |
|---|---|---|---|---|
| 1 | default-src | Fetch | Fallback for all other fetch directives | None (browser default) |
| 2 | script-src | Fetch | JavaScript and WebAssembly resources | default-src |
| 3 | style-src | Fetch | CSS stylesheets | default-src |
| 4 | img-src | Fetch | Images and favicons | default-src |
| 5 | connect-src | Fetch | XHR, Fetch, WebSocket, EventSource | default-src |
| 6 | font-src | Fetch | Web fonts via @font-face | default-src |
| 7 | frame-src | Fetch | <iframe> and <frame> sources | child-src then default-src |
| 8 | media-src | Fetch | <audio>, <video>, <track> elements | default-src |
| 9 | object-src | Fetch | <object>, <embed> plugins | default-src |
| 10 | worker-src | Fetch | Worker, SharedWorker, ServiceWorker | child-src then script-src then default-src |
| 11 | base-uri | Document | URLs for <base> element | No fallback (allows any) |
| 12 | form-action | Navigation | Form submission target URLs | No fallback (allows any) |
| 13 | frame-ancestors | Navigation | Who can embed this page (replaces X-Frame-Options) | No fallback (allows any) |
| 14 | upgrade-insecure-requests | Document | Auto-upgrade HTTP to HTTPS | Disabled |
| 15 | report-to | Reporting | Where to send violation reports (replaces report-uri) | No reporting |
Source Expression Keywords
| Keyword | Meaning | Use In |
|---|---|---|
'self' | Same origin only | Any directive |
'none' | Block all resources of this type | Any directive |
'unsafe-inline' | Allow inline <script> and <style> (dangerous) | script-src, style-src |
'unsafe-eval' | Allow eval(), new Function(), setTimeout(string) | script-src |
'nonce-{random}' | Allow elements with matching nonce attribute | script-src, style-src |
'sha256-{hash}' | Allow elements matching the hash | script-src, style-src |
'strict-dynamic' | Trust scripts loaded by already-trusted scripts | script-src |
'unsafe-hashes' | Allow inline event handlers matching hashes | script-src |
https: | Allow any HTTPS origin | Any directive |
data: | Allow data: URIs | Any directive |
Decision Tree
START: What is your CSP deployment scenario?
├── New application (no existing inline scripts)?
│ ├── YES → Deploy strict nonce-based CSP immediately (see Step 1)
│ └── NO ↓
├── Existing app with inline scripts you control?
│ ├── YES → Add nonces to all inline scripts, then deploy strict CSP (see Step 2)
│ └── NO ↓
├── Existing app with third-party inline scripts?
│ ├── YES → Use hash-based CSP for static scripts + nonces for dynamic (see Step 3)
│ └── NO ↓
├── Legacy app that cannot be modified?
│ ├── YES → Deploy allowlist-based CSP as interim measure, plan migration to strict CSP
│ └── NO ↓
├── Need to prevent clickjacking only?
│ ├── YES → Use frame-ancestors 'none' or 'self' (Step 5)
│ └── NO ↓
└── DEFAULT → Start with report-only mode to audit current resource loading (Step 1)
Step-by-Step Guide
1. Deploy CSP in report-only mode
Start by deploying a strict policy in report-only mode to discover what would break without actually blocking anything. [src6]
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self';
object-src 'none';
base-uri 'none';
report-to csp-endpoint;
Verify: Open browser DevTools > Console -- CSP violations appear as warnings with [Report Only] prefix.
2. Add nonces to all inline scripts
Generate a cryptographically random nonce per request and add it to every inline <script> tag. [src5]
Content-Security-Policy: script-src 'nonce-dGhpcyBpcyBhIG5vbmNl' 'strict-dynamic'
Verify: Inline scripts without a nonce should be blocked. Check console for Refused to execute inline script errors.
3. Build a strict CSP policy
Combine the essential directives for a production-ready strict CSP. [src3]
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM}';
img-src 'self' https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-src 'none';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
report-to csp-endpoint;
Verify: Run your CSP through Google's CSP Evaluator at https://csp-evaluator.withgoogle.com/.
4. Switch from report-only to enforcement
After monitoring report-only mode for at least 1-2 weeks with no unexpected violations, switch to enforcement. [src6]
curl -sI https://your-site.com | grep -i content-security-policy
Verify: Should return the enforced policy (not report-only).
5. Configure frame-ancestors for clickjacking protection
Replace the legacy X-Frame-Options header with CSP frame-ancestors. [src1]
# Prevent all framing
Content-Security-Policy: frame-ancestors 'none';
# Allow same-origin framing only
Content-Security-Policy: frame-ancestors 'self';
# Allow specific origins
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;
Verify: Try embedding your page in an <iframe> on a different origin -- it should be blocked.
Code Examples
Express.js/Node.js: Helmet CSP with Per-Request Nonces
const express = require('express'); // ^4.18.0
const helmet = require('helmet'); // ^8.0.0
const crypto = require('crypto');
const app = express();
// Generate per-request nonce middleware
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
// Configure CSP via Helmet
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
(req, res) => `'nonce-${res.locals.cspNonce}'`,
"'strict-dynamic'"
],
styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
imgSrc: ["'self'", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: []
}
}
}));
// In templates: <script nonce="<%= cspNonce %>">...</script>
Django: django-csp Configuration
# settings.py -- using django-csp >= 4.0
# pip install django-csp
MIDDLEWARE = ["csp.middleware.CSPMiddleware", ...]
CONTENT_SECURITY_POLICY = {
"DIRECTIVES": {
"default-src": ["'self'"],
"script-src": ["'strict-dynamic'"], # nonces added automatically
"style-src": ["'self'"],
"img-src": ["'self'", "https:"],
"connect-src": ["'self'"],
"font-src": ["'self'"],
"object-src": ["'none'"],
"base-uri": ["'none'"],
"form-action": ["'self'"],
"frame-ancestors": ["'none'"],
"upgrade-insecure-requests": True,
}
}
# In templates: {% load csp %} <script nonce="{% csp_nonce %}">...</script>
Nginx: CSP Header Configuration
# /etc/nginx/snippets/csp-headers.conf
# Hash-based CSP for static sites (Nginx cannot generate nonces natively)
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'sha256-{HASH_OF_INLINE_SCRIPT}';
style-src 'self';
img-src 'self' https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
" always;
Apache: CSP Header Configuration
# .htaccess or httpd.conf
Header always set Content-Security-Policy "\
default-src 'self'; \
script-src 'self' 'sha256-{HASH_OF_INLINE_SCRIPT}'; \
style-src 'self'; \
img-src 'self' https:; \
font-src 'self'; \
connect-src 'self'; \
object-src 'none'; \
base-uri 'none'; \
form-action 'self'; \
frame-ancestors 'none'; \
upgrade-insecure-requests"
Anti-Patterns
Wrong: Using unsafe-inline with script-src
# BAD -- unsafe-inline completely negates CSP's XSS protection
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
# Any XSS payload can execute inline scripts freely
Correct: Nonce-based script-src
# GOOD -- only scripts with the correct nonce execute
Content-Security-Policy: script-src 'nonce-abc123def456' 'strict-dynamic'
# XSS payloads cannot guess the per-request nonce
Wrong: Wildcard sources in script-src
# BAD -- allows scripts from ANY origin
Content-Security-Policy: default-src *; script-src *
Correct: Explicit origin allowlist or nonces
# GOOD -- use nonce-based CSP with strict-dynamic
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'
Wrong: Overly permissive default-src
# BAD -- default-src https: allows loading from ANY HTTPS origin
Content-Security-Policy: default-src https:
# Attackers can host payloads on any HTTPS domain they control
Correct: Restrictive default-src with explicit overrides
# GOOD -- lock down default, open specific directives as needed
Content-Security-Policy: default-src 'none'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self'; img-src 'self' https:; font-src 'self'; connect-src 'self'
Wrong: CSP via meta tag for frame-ancestors
<!-- BAD -- frame-ancestors, report-uri, report-to, and sandbox
are IGNORED in meta tags -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'none'; report-uri /csp-report">
Correct: CSP via HTTP header
# GOOD -- all directives work via HTTP header
Content-Security-Policy: frame-ancestors 'none'; report-to csp-endpoint
Wrong: Using unsafe-eval for convenience
# BAD -- allows eval(), new Function(), setTimeout(string)
Content-Security-Policy: script-src 'self' 'unsafe-eval'
Correct: Refactoring to avoid eval
// GOOD -- replace eval() with safe alternatives
// Instead of: setTimeout('doSomething()', 1000)
setTimeout(doSomething, 1000); // pass function reference
// Instead of: new Function('return ' + userInput)
JSON.parse(userInput); // for data parsing
Common Pitfalls
- Forgetting report-only first: Deploying CSP in enforcement mode immediately breaks inline scripts, third-party widgets, and analytics. Fix: Always deploy with
Content-Security-Policy-Report-Onlyfirst and monitor for 1-2 weeks. [src6] - Nonce reuse across requests: Serving the same nonce for all requests allows attackers who discover it to bypass CSP. Fix: Generate a new cryptographically random nonce for every HTTP response using
crypto.randomBytes(16). [src5] - Missing object-src and base-uri: Omitting
object-srcallows plugin injection; omittingbase-uriallows<base>tag hijacking. Fix: Always includeobject-src 'none'; base-uri 'none'. [src3] - Allowlist bypasses via JSONP/Angular: Allowlisting a CDN that hosts JSONP endpoints lets attackers load script gadgets that bypass CSP. Fix: Use nonce-based CSP with
strict-dynamicinstead of domain allowlists. [src5] - CSP on meta tag limitations: Using
<meta http-equiv="Content-Security-Policy">ignoresframe-ancestors,report-uri,report-to, andsandbox. Fix: Set CSP via HTTP response headers. [src2] - Mixed content after upgrade-insecure-requests: The directive does not upgrade WebSocket (
ws://) connections. Fix: Explicitly usewss://for WebSocket connections. [src1] - Breaking service workers: Restrictive
worker-srcorscript-srcwithout accounting for service worker scope blocks SW registration. Fix: Includeworker-src 'self'if you use service workers. [src2] - report-uri deprecation:
report-uriis deprecated in CSP Level 3 in favor ofreport-to. Fix: Include both during the transition period. [src4]
Diagnostic Commands
# Check current CSP headers of a live site
curl -sI https://your-site.com | grep -i content-security-policy
# Check CSP with full headers
curl -sI https://your-site.com | grep -iE '(content-security|report)'
# Generate SHA-256 hash of an inline script for hash-based CSP
echo -n 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64
# Validate CSP: visit https://csp-evaluator.withgoogle.com/
# Scan for inline scripts that need nonces
grep -rn '<script>' --include="*.html" --include="*.ejs" .
# Scan for inline event handlers that CSP will block
grep -rn 'onclick=\|onload=\|onerror=\|onsubmit=' --include="*.html" .
Version History & Compatibility
| CSP Level | Status | Browser Support | Key Features |
|---|---|---|---|
| CSP Level 3 | W3C Working Draft (June 2025) | Chrome 59+, Firefox 58+, Safari 15.4+, Edge 79+ | strict-dynamic, report-to, worker-src, manifest-src, nonce/hash enhancements |
| CSP Level 2 | W3C Recommendation | Chrome 40+, Firefox 31+, Safari 10+, Edge 15+ | script-src, style-src, base-uri, form-action, frame-ancestors, report-uri, nonce/hash |
| CSP Level 1 | W3C Recommendation (deprecated) | Chrome 25+, Firefox 23+, Safari 7+, Edge 12+ | Basic default-src, script-src, img-src, style-src, connect-src |
| Trusted Types | W3C Draft | Chrome 83+, Firefox behind flag | require-trusted-types-for 'script', DOM sink protection |
| Reporting API v1 | W3C Draft | Chrome 96+, Edge 96+ | report-to with Reporting-Endpoints header |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Any web application serving HTML to browsers | Static file server with no HTML (pure API) | CORS headers for API protection |
| Need XSS defense-in-depth beyond output encoding | CSP is your only XSS defense | Output encoding + CSP together |
| Controlling third-party script loading (analytics, ads) | You trust all content from all origins | Still use CSP -- trust boundaries change |
| Preventing clickjacking (frame-ancestors) | Only need clickjacking protection | X-Frame-Options as legacy fallback, but prefer CSP |
| Compliance requires CSP (PCI DSS, SOC 2) | Internal-only tool with no external access | CSP still recommended but lower priority |
Important Caveats
strict-dynamictrusts scripts loaded by nonce-approved scripts -- if a trusted script has a gadget vulnerability (e.g., JSONP endpoint, prototype pollution), it can be exploited to bypass CSP- CSP via
<meta>tag does not supportframe-ancestors,report-uri,report-to, orsandbox-- use HTTP headers for full protection - Internet Explorer does not support
Content-Security-Policy-- only the non-standardX-Content-Security-Policy(IE 10-11, limited support) upgrade-insecure-requestsdoes not upgrade WebSocket connections (ws://towss://) -- handle this explicitly in application code- Hash-based CSP requires recalculating hashes whenever inline script content changes -- prefer nonces for dynamic applications
- Multiple CSP headers are intersected (most restrictive wins), not merged -- adding a second header can only restrict, never relax, the policy