Why Content Security Policy is your first line of defense
CSP is not a firewall, it's a whitelist of what resources your app can load. Without CSP, any injected script (XSS) executes with your domain's permissions: access to cookies, localStorage, can make requests to your API. With CSP, even if an attacker injects <script>alert('hack')</script>, the browser blocks it if it doesn't meet the policy. It's defense in depth: you assume your code has bugs, but you limit the damage. CSP also prevents clickjacking with frame-ancestors, forces HTTPS with upgrade-insecure-requests, and blocks obsolete plugins with object-src 'none'. A well-configured policy reduces XSS risk by 90%.
Essential CSP directives and what they control
default-src: Fallback for everything you don't specify; start with 'self'. script-src: Where scripts can come from; avoid 'unsafe-inline' (allows XSS), use nonces or hashes. style-src: Same for CSS; third-party libraries often need 'unsafe-inline'. img-src: Image sources; data: allows base64, https: allows any HTTPS. connect-src: Allowed APIs and WebSockets; limit to your domains. frame-ancestors: Who can embed your page; 'none' prevents clickjacking. base-uri: Limits <base> tag (rare but real attack vector). form-action: Where forms can submit.
Migrating to CSP without breaking production
Start with Content-Security-Policy-Report-Only: logs violations without blocking anything. Configure report-uri /csp-violations and analyze what resources load (Google Fonts, CDNs, analytics). Add domains to whitelist gradually. Common findings: inline styles in React components, inline event handlers (onclick='...'), eval() in old libraries. Solutions: extract inline styles to files, use programmatic event listeners, replace libraries using eval. Once Report-Only shows no violations for 1 week, activate the real header. Use dynamic nonces (generate random per request) for critical inline scripts.
Common mistakes when configuring CSP
Using 'unsafe-inline' and 'unsafe-eval' by default: Negates 80% of CSP protection; only use if no alternative and you understand the risk. Forgetting 'self' in default-src: If you set default-src https:, your own scripts fail; always include 'self'. Not specifying protocol in domains: cdn.example.com allows HTTP and HTTPS; better https://cdn.example.com. Whitelist too broad: script-src https: allows any HTTPS, including compromised CDNs; be specific. Not testing in all browsers: Safari has CSP quirks; test before production. Not versioning policies: Keep your CSP in git; you know what changed if something breaks.
Reference table: most-used CSP directives
Quick reference for the Content Security Policy directives you'll configure 90% of the time, what resource each one controls, and a safe example value.
| Directive | What it controls | Example value |
|---|---|---|
default-src | Fallback for any -src directive you don't define | 'self' |
script-src | Valid sources for JavaScript (<script>, inline events, eval) | 'self' 'nonce-{RANDOM}' |
style-src | Valid sources for CSS and stylesheets | 'self' |
img-src | Valid sources for images and favicons | 'self' data: https: |
connect-src | Targets for fetch, XHR, WebSocket and EventSource | 'self' https://api.example.com |
font-src | Valid sources for web fonts (@font-face) | 'self' https://fonts.gstatic.com |
frame-ancestors | Who can embed your page in an iframe (anti-clickjacking) | 'none' |
base-uri | Restricts valid URLs for the <base> tag | 'self' |
form-action | Targets a <form> is allowed to submit to | 'self' |
object-src | Sources for <object>, <embed> and plugins | 'none' |
Rule of thumb: start with default-src 'self', set object-src 'none' and frame-ancestors 'none', then only open specific origins as your Report-Only mode asks for them.