Most developers know they should add a Content Security Policy header to their apps. Few have one that’s actually doing anything useful. Either it’s so permissive it defeats the purpose, or it breaks half the site and gets removed in frustration.
This guide walks through what CSP does, which directives matter, and how to write a header that blocks real attacks without breaking your app.
What CSP Actually Does
A Content Security Policy tells the browser where it’s allowed to load resources from. Script from this domain only. Styles from that CDN. Images from anywhere. No inline scripts.
When a browser receives a CSP header, it enforces those rules before executing anything. If a cross-site scripting (XSS) attack injects a malicious script into your HTML, CSP blocks it at the browser level — even if the script got past your input sanitization.
That’s the core value: CSP is your second line of defense when input validation fails.
The Directives That Matter
CSP has dozens of directives, but most production headers only need six:
| Directive | What it restricts | Common mistake |
|---|---|---|
default-src |
Fallback for any resource type not explicitly listed | 环境 'self', then forgetting fonts and frames aren’t covered |
script-src |
JavaScript source URLs and inline execution | Adding 'unsafe-inline' to silence console errors |
style-src |
CSS source URLs and inline style blocks | Forgetting your CDN or inline styles injected by JS libraries |
img-src |
Image sources including data URIs | Missing data: for base64 images, blob: for canvas exports |
connect-src |
XHR, fetch, WebSocket, and EventSource destinations | Forgetting analytics endpoints and API subdomains |
frame-ancestors |
Which origins can embed your site in an <iframe> |
Skipping it entirely — leaving clickjacking wide open |
frame-ancestors replaces the older X-Frame-Options header and is worth adding even before you have full CSP coverage elsewhere.
为什么 unsafe-inline Destroys the Point
When you add 'unsafe-inline' 到 script-src, you’re telling the browser: execute any script tag you find, wherever it came from. That includes scripts injected by XSS attacks.
'unsafe-eval' is worse — it allows eval(), Function(),并且 setTimeout("string"), which are common attack vectors even in otherwise-clean codebases.
A policy with either of these documents your sources without providing any meaningful injection protection. The attacker doesn’t care where your legitimate scripts load from if they can inject their own inline.
The Right Way: Nonces and Hashes
If you have legitimate inline scripts, you have two options that don’t surrender your policy:
Nonces generate a unique token per page load. The server adds it to the CSP header and to the inline script’s nonce attribute. Only scripts with a matching nonce execute.
<!-- CSP header -->
Content-Security-Policy: script-src 'nonce-abc123xyz'
<!-- Inline script with matching nonce -->
<script nonce="abc123xyz">
window.analyticsId = '...';
</script>
Hashes compute a SHA-256 fingerprint of the exact script content. Add that hash to script-src and the browser runs that specific script — but nothing else.
Content-Security-Policy: script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='
Nonces work better for dynamic pages where content changes per request. Hashes suit static scripts that never change between deploys.
Deploy with Report-Only Mode First
Adding CSP to an existing site without testing is how you break it in production. Start with the report-only header instead:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-{random}'; report-uri /csp-violations
The browser applies the rules and reports violations — it doesn’t block anything yet. That violation log tells you exactly what you need to add to the policy before you flip it to enforcement mode.
Most CSP deployments that break sites skipped this step.
A Real CSP for a Typical SaaS App
Here’s a production-ready header for an app using a CDN, Google Analytics, and Stripe. Each directive is annotated:
Content-Security-Policy:
# Tight default: only load from your own origin
default-src 'self';
# Scripts: your origin, GA tag manager via nonce, Stripe.js
script-src 'self' https://www.googletagmanager.com https://js.stripe.com 'nonce-{SERVER_NONCE}';
# Styles: your origin and Google Fonts CSS
style-src 'self' https://fonts.googleapis.com;
# Fonts: your origin and Google Fonts CDN
font-src 'self' https://fonts.gstatic.com;
# Images: your origin, CDN subdomain, and base64 data URIs
img-src 'self' https://cdn.yourdomain.com data:;
# Fetch/XHR: your API, GA collection endpoint, Stripe API
connect-src 'self' https://www.google-analytics.com https://api.stripe.com;
# Stripe renders its fields in an iframe
frame-src https://js.stripe.com;
# Nobody should be framing your app
frame-ancestors 'none';
# Block <object> and <embed> entirely
object-src 'none';
# Force HTTP requests to upgrade to HTTPS
upgrade-insecure-requests;
将 {SERVER_NONCE} with a cryptographically random value generated per request — e.g., base64_encode(random_bytes(16)) in PHP or crypto.randomBytes(16).toString('base64') in Node.
Common CSP Mistakes
Wildcards that are too broad. script-src * allows scripts from any origin. You might as well have no script policy at all.
Forgetting subdomains. 'self' only covers your exact origin. https://api.yourdomain.com is a different origin and needs an explicit entry under connect-src.
Skipping frame-ancestors. Clickjacking protection is independent of XSS protection. Add it separately from the rest of your directive set.
Setting CSP in multiple places. If your CDN and your app both set a CSP header, behavior gets unpredictable. Set it in one layer — usually your origin server or reverse proxy.
使用 report-uri without a handler. Violations generate POST requests to your endpoint on every affected page load. Either handle them or point to a service like Report URI.
Build Your Header Without the Guesswork
Tracking every domain your page loads resources from across scripts, styles, fonts, images, and API calls gets tedious quickly. An online CSP header generator lets you configure each directive visually and outputs a production-ready header you can paste directly into your server config.
Use it as a starting point, then audit your actual network requests in DevTools to catch anything the generator missed.
A CSP that’s too loose is decoration. One that’s too strict breaks your users. Start in report-only mode, tighten from there, and use nonces where inline scripts are unavoidable. Your policy should make an attacker’s job harder — not yours.
