What Is Content Security Policy?
Content Security Policy (CSP) is an HTTP response header that tells the browser which resources are allowed to load on a page. It is the most powerful defense against Cross-Site Scripting (XSS) — attacks where malicious scripts are injected into your page and run in the victim's browser.
Without CSP, any JavaScript that ends up on your page — whether injected through an XSS flaw, a compromised CDN, or an evil browser extension — can access cookies, read the DOM, make network requests, and exfiltrate data.
CSP breaks this by creating an explicit allowlist. The browser refuses to execute any script, load any image, or make any network request that isn't on the list.
Header Syntax
Content-Security-Policy: directive1 source1 source2; directive2 source3;
Each directive controls a resource type. Each source defines where that resource can come from. Semicolons separate directives.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
Key Directives
default-src — The Fallback
default-src sets the default policy for any resource type that doesn't have its own directive. Start with default-src 'self' — only allow resources from your own origin — then add specific permissions where needed.
script-src — JavaScript
The most critical directive. It controls which JavaScript can execute.
| Source | Meaning |
|---|---|
| `'self'` | Same origin as the page |
| `https://cdn.example.com` | Specific HTTPS host |
| `'nonce-abc123'` | Inline script with matching nonce |
| `'sha256-<hash>'` | Inline script matching a specific hash |
| `'unsafe-inline'` | Allow all inline scripts (disables most XSS protection) |
| `'unsafe-eval'` | Allow eval() and Function() constructors |
| `'strict-dynamic'` | Allow scripts loaded by trusted scripts |
Never use 'unsafe-inline' in script-src — it negates the XSS protection that CSP is designed to provide.
style-src — Stylesheets
Controls CSS loading. Unlike scripts, inline <style> tags and style= attributes are less dangerous, but 'unsafe-inline' in style-src still enables CSS injection attacks (data exfiltration via CSS selectors).
img-src — Images
img-src 'self' https: data:;
data: allows inline base64 images. https: allows images from any HTTPS source. This is typically the most permissive directive because image loads are lower risk.
connect-src — Fetch, XHR, WebSocket
Controls which URLs your JavaScript can make network requests to. Critical for preventing data exfiltration:
connect-src 'self' https://api.example.com wss://realtime.example.com;
frame-ancestors — Clickjacking Defense
frame-ancestors replaces the old X-Frame-Options header and is more flexible:
frame-ancestors 'none'; # Block all framing
frame-ancestors 'self'; # Only allow same-origin framing
frame-ancestors https://app.example.com; # Allow specific parent
Deployment Strategy: Report-Only First
Never deploy a CSP in enforcing mode as your first step. You will break things.
Start with Content-Security-Policy-Report-Only. This header sends violation reports but does not block anything:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com;
report-uri /csp-violations
Build a simple endpoint to collect violations:
# Django view to collect CSP violation reports
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import logging
logger = logging.getLogger(__name__)
@csrf_exempt
def csp_violations(request):
if request.method == 'POST':
try:
report = json.loads(request.body)
logger.info('CSP violation', extra=report)
except json.JSONDecodeError:
pass
return HttpResponse(status=204)
Collect violations for 1-2 weeks across all your pages. Then refine your policy to add legitimate sources before switching to the enforcing header.
Handling Inline Scripts
Inline scripts — <script> tags with code directly inside, and onclick= attributes — are the primary vector for XSS. CSP's power against XSS comes from blocking inline scripts. But many sites rely on them heavily.
Nonce-Based Approach
A nonce (number used once) is a random token generated per request. You add it to your CSP header and to each trusted inline script. The browser only executes inline scripts whose nonce matches the header.
# Django middleware: generate nonce per request
import secrets
from django.utils.decorators import method_decorator
class CSPNonceMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.csp_nonce = secrets.token_hex(16)
response = self.get_response(request)
response['Content-Security-Policy'] = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{request.csp_nonce}' 'strict-dynamic';"
)
return response
<!-- Template: use the nonce on your inline scripts -->
<script nonce="{{ request.csp_nonce }}">
initApp();
</script>
An attacker who injects <script>alert(1)</script> cannot know the nonce for this request — their script won't execute.
Hash-Based Approach
For static inline scripts that don't change, use a hash:
# Generate SHA-256 hash of inline script content
echo -n 'initApp();' | openssl dgst -sha256 -binary | openssl base64
# Output: <hash>
Content-Security-Policy: script-src 'self' 'sha256-<hash>'
The browser computes the hash of each inline script and only executes ones that match a hash in the CSP header.
strict-dynamic
'strict-dynamic' propagates trust: a script allowed by nonce or hash can load additional scripts dynamically. This enables modern frameworks (React, Vue) that inject scripts at runtime, without needing to allowlist every chunk URL.
Common Mistakes
Mistake 1: Wildcard sources
script-src *; # Allows scripts from anywhere — useless CSP
Mistake 2: unsafe-inline with hashes
When you add a hash or nonce, 'unsafe-inline' is automatically ignored by modern browsers — but it's still bad practice and confusing.
Mistake 3: Forgetting connect-src
A strict script-src that blocks XSS injection is undermined if connect-src * allows injected scripts to exfiltrate data to any server.
Mistake 4: CSP for SPAs without strict-dynamic
Single-page applications typically inject <script> tags via module bundlers. Without 'strict-dynamic', you'd need to allowlist every chunk hash. Use 'nonce-...' 'strict-dynamic' instead.