What CSP does and why it matters
Content Security Policy is a browser security mechanism that restricts which resources a page can load and execute. It is delivered as an HTTP response header (Content-Security-Policy) and acts as a whitelist: the browser blocks anything not explicitly allowed.
Without CSP, any script injected into your page (via XSS, a compromised third-party library, or a man-in-the-middle attack) executes with the full privileges of your origin. CSP limits the blast radius of injection attacks by preventing the browser from executing inline scripts, loading scripts from unauthorized origins, or submitting form data to attacker-controlled endpoints.
CSP also mitigates clickjacking (via frame-ancestors), mixed content (by upgrading insecure requests), and data exfiltration (by restricting connect-src and form-action).
Directives reference
Each directive controls a specific resource type. If a directive is not set, the browser falls back to default-src. If default-src is also absent, the resource is allowed from any origin.
default-src: Fallback for all fetch directives not explicitly listed. Set this to'self'as a baseline, then open up specific directives as needed.script-src: Controls JavaScript execution. This is the most security-critical directive. Avoid'unsafe-inline'and'unsafe-eval'if at all possible; use nonces or hashes instead.style-src: Controls CSS loading and inline styles.'unsafe-inline'is often needed for legacy CSS but should be replaced with hashes or nonces over time.img-src: Controls image sources. Often needsdata:for inline images and specific CDN origins.connect-src: Controls fetch, XMLHttpRequest, WebSocket, and EventSource destinations. Critical for preventing data exfiltration -- restrict to your own API origins.frame-ancestors: Controls which origins can embed this page in an iframe. ReplacesX-Frame-Options. Does not fall back todefault-src.object-src: Controls Flash, Java applets, and other plugin content. Set to'none'on all modern sites.base-uri: Restricts which URLs can appear in the<base>element. Set to'self'or'none'to prevent base-tag hijacking attacks.form-action: Restricts where forms can submit data. Does not fall back todefault-src.font-src: Controls web font loading. Typically needs your own origin plus any font CDNs.media-src: Controls audio and video sources.worker-src: Controls Web Workers and Service Workers. Falls back toscript-src, thendefault-src.
Common mistakes
'unsafe-inline'
Adding 'unsafe-inline' to script-src disables the primary XSS protection that CSP provides. Any injected <script> tag will execute. If you need inline scripts, use a per-request nonce:
Content-Security-Policy: script-src 'nonce-abc123def456'
<script nonce="abc123def456">
// This script runs. Injected scripts without the nonce do not.
</script>
The nonce must be cryptographically random and regenerated on every response. A static nonce is equivalent to 'unsafe-inline'.
'unsafe-eval'
Permits eval(), Function(), setTimeout('string'), and similar dynamic code execution. This is often required by older template engines and some frameworks. Audit your dependencies and replace eval usage where possible. CSP Level 3 introduces 'unsafe-eval' alternatives like 'wasm-unsafe-eval' for WebAssembly without opening the door to full eval.
Overly broad sources
Whitelisting an entire CDN origin like https://cdn.example.com means any file on that CDN can be loaded as a script. If the CDN hosts user-uploaded content, an attacker can upload a malicious JS file and exploit your whitelist. Prefer specific paths or use strict-dynamic with nonces.
report-uri vs report-to
CSP can send violation reports to a URL you control, which is invaluable for debugging and monitoring.
report-uri (CSP Level 2) sends a JSON POST to a specified endpoint when a violation occurs:
Content-Security-Policy: default-src 'self'; report-uri /csp-report
report-to (CSP Level 3) uses the Reporting API, which is more flexible and supports batching:
Report-To: {"group":"csp","max_age":86400,"endpoints":[{"url":"/csp-report"}]}
Content-Security-Policy: default-src 'self'; report-to csp
Browser support for report-to is still inconsistent. For maximum coverage, include both directives. They do not conflict.
CSP Levels: 1, 2, and 3
CSP Level 1 introduced the basic directive model and source lists. Still widely supported.
CSP Level 2 added nonces, hashes, base-uri, form-action, frame-ancestors, and report-uri. This is the baseline you should target.
CSP Level 3 added strict-dynamic (trust propagation from nonced scripts to scripts they load), report-to, worker-src, and navigate-to. strict-dynamic is the recommended approach for complex applications because it reduces whitelist maintenance -- you only need to nonce the entry-point scripts, and they can dynamically load additional scripts.
Practical examples
Starter policy
Begin with a restrictive baseline in report-only mode to discover what your site actually needs:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
report-uri /csp-report
Deploy this header and monitor your report endpoint. Every blocked resource generates a report with the violated directive, the blocked URI, and the document URL. This tells you exactly what to whitelist.
Iterative tightening
After reviewing reports, add the specific origins your site needs:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'nonce-{random}';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /csp-report
Key points: each inline script and style tag gets a server-generated nonce. upgrade-insecure-requests rewrites HTTP subresource URLs to HTTPS automatically. form-action prevents form submissions to third-party origins.
Nginx configuration
# /etc/nginx/snippets/csp.conf
add_header Content-Security-Policy
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"
always;
Apache configuration
# .htaccess or httpd.conf
Header always set Content-Security-Policy \
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"
Traefik middleware
# traefik dynamic config
http:
middlewares:
csp:
headers:
contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"