How Content-Security-Policy (CSP) Works: Complete Guide
CSP is the single most effective browser-enforced defense against cross-site scripting. Here's how the directive syntax works and how to roll one out safely.
The Core Idea
By default, a browser will execute any script tag present in a page's HTML, regardless of where it came from — including one an attacker managed to inject via an unescaped form field or vulnerable third-party widget. CSP flips this to a default-deny model: only sources you've explicitly allow-listed can supply content, and inline scripts are blocked by default unless specifically permitted.
Key CSP Directives
| Directive | Controls |
|---|---|
default-src | Fallback source list for any directive not explicitly set |
script-src | Where JavaScript may be loaded from |
style-src | Where CSS may be loaded from |
img-src | Where images may be loaded from |
connect-src | Allowed targets for fetch/XHR/WebSocket calls |
frame-ancestors | Which sites may embed this page in an iframe (replaces X-Frame-Options) |
object-src | Controls plugins like Flash/Java applets — usually set to 'none' |
base-uri | Restricts what <base> tags can set, preventing base-tag hijacking |
Writing a Real Policy
A reasonably strict but workable starting policy for a typical site using a CDN and Google Analytics:
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'self'; object-src 'none'; base-uri 'self'
Notice everything is scoped to 'self' by default, with specific third-party origins added only where actually needed — the opposite of a wildcard * approach, which defeats most of the protection.
Report-Only Mode: Test Before You Enforce
Deploying a strict CSP directly to production risks silently breaking functionality that depends on a resource you didn't account for. Use the Content-Security-Policy-Report-Only header instead first — the browser evaluates the policy and logs violations to the console (and optionally to a report-uri endpoint) without actually blocking anything. Once you've reviewed violation reports and confirmed no legitimate functionality is affected, switch to the enforcing header.
Common Pitfalls
- Using 'unsafe-inline': This defeats most of CSP's XSS protection since it allows any inline script to run, including an injected one. Prefer nonces or hashes instead (see below).
- Forgetting third-party widgets: Chat widgets, analytics, ad scripts, and embedded maps often load additional resources dynamically from domains not in your original allow-list.
- Not testing report-only first: Jumping straight to an enforcing policy on a complex site frequently breaks something that only surfaces after deployment.
- Overly broad wildcards: A directive like
script-src *technically satisfies "having a CSP" while providing almost no actual protection.
Nonces & Hashes for Inline Scripts
If you genuinely need inline scripts, CSP supports cryptographic nonces (a random value generated fresh per page load and matched in both the header and the script tag) or hashes (a SHA hash of the exact script content). Both let specific, known-good inline scripts execute while still blocking any attacker-injected ones, since an attacker can't predict the nonce or produce a script matching a pre-approved hash.
Example nonce usage: script-src 'nonce-r4nd0mVal123' paired with <script nonce="r4nd0mVal123"> in the HTML.