Cross-site scripting (XSS) remains one of the most common web vulnerabilities. Sanitizing output and using framework escaping help, but defense in depth means assuming some untrusted HTML or script might still reach the page. Content Security Policy (CSP) is a browser-enforced contract: you declare which origins and resources may load, execute, and connect—and the browser blocks everything else.
CSP is not a silver bullet. Misconfigured policies break legitimate features; overly permissive policies add little value. This guide explains how CSP works, the main directives, rollout strategies (including nonces and hashes), reporting, and limitations you should plan for.
What CSP does
CSP is delivered as an HTTP response header (Content-Security-Policy) or, less ideally, a <meta http-equiv="Content-Security-Policy"> tag (some directives do not work in meta form).
The browser parses policy directives and enforces them on the document and its subresources before execution. Inline script blocked by CSP does not run—even if an attacker injected it.
CSP also restricts:
- Where scripts and styles may load from
- Whether inline script and
evalare allowed - Which URLs
fetch, XHR, WebSocket, and EventSource may target - Embedding (frames, ancestors)
- Form submission targets
- And more via additional directives
Think of CSP as a allowlist for browser capabilities on your pages.
Core directives for typical web apps
default-src
Fallback when more specific directives are absent. Often set to 'self' as a baseline.
````
Content-Security-Policy: default-src 'self';
script-src
Controls JavaScript sources. Critical for XSS mitigation.
````
script-src 'self' https://cdn.example.com;
Inline script (<script>alert(1)</script>) is blocked unless you allow:
'unsafe-inline'(weak—avoid in production)- Nonces:
script-src 'nonce-random123'plus matchingnonceattribute on trusted inline scripts - Hashes:
script-src 'sha256-abc...'for known inline script content
Dynamic code:
'unsafe-eval'allowseval,new Function, some template libraries—avoid if possible
style-src
Controls CSS. Inline style="" attributes and <style> blocks follow similar rules to scripts. 'unsafe-inline' is common for CSS because many apps use inline styles; tightening style CSP is harder than script CSP.
img-src, font-src, media-src
Control images, fonts, audio/video. Often include 'self' and CDNs you use.
connect-src
Restricts fetch, XMLHttpRequest, WebSocket, EventSource. Important to prevent exfiltration to attacker domains even if script injection partially succeeds.
````
connect-src 'self' https://api.example.com;
frame-ancestors
Replaces older X-Frame-Options for clickjacking protection. Controls who may embed your page in an iframe.
````
frame-ancestors 'self' https://partner.example.com;
Use 'none' to forbid all framing.
base-uri
Restricts <base href>—prevents attackers from changing relative URL resolution.
form-action
Where forms may submit. Limits credential posting to unexpected endpoints.
object-src
Historically 'none' recommended—blocks plugins like Flash.
Keyword sources
| Source | Meaning |
|--------|---------|
| 'self' | Same origin as the document |
| 'none' | Block all (for a directive) |
| 'unsafe-inline' | Allow inline script/style (script: dangerous) |
| 'unsafe-eval' | Allow eval and similar |
| 'strict-dynamic' | Trust scripts loaded by already-trusted scripts (nonce/hash chain) |
| https: | Any HTTPS URL (broad) |
| data: | data: URLs (sometimes needed for images/fonts) |
| blob: | blob: URLs (workers, media) |
| Nonce | Random per-request value on script tag and header |
| Hash | SHA hash of allowed inline script body |
Prefer nonces or hashes over 'unsafe-inline' for scripts.
A minimal starter policy
For a static site with no inline scripts, all assets same-origin:
````
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
Add CDN origins explicitly when you introduce them. Each new third-party script (analytics, chat widget) is a policy change.
Nonces in dynamic applications
Server-rendered apps generate a cryptographically random nonce per request:
- Generate
nonce(e.g., 16+ bytes base64) - Set header:
script-src 'nonce-{value}' 'strict-dynamic'(plus any static host allowlist as needed) - Add
nonce="{value}"to trusted<script>tags - Avoid inline event handlers (
onclick=)—they are not nonceable the same way
Frameworks (Next.js, Rails with secure headers gems) automate nonce injection. Client-side-only SPAs without SSR require hash-based or bundled-only script strategies.
Hashes for static inline snippets
If you have a small fixed inline script, compute SHA-256 hash of the exact script body (including whitespace as served):
````
script-src 'sha256-abc123...=';
Any change to the script requires updating the hash.
strict-dynamic and third-party scripts
'strict-dynamic' allows scripts loaded by a nonce-trusted root script to load further scripts without listing every CDN subdomain—useful for module loaders. Combine with nonces on entry scripts; avoid falling back to broad host allowlists that negate benefits.
Understand browser support and test across targets you support.
Report-Only mode
Roll out with Report-Only before enforcing:
````
Content-Security-Policy-Report-Only: script-src 'self'; report-uri /csp-report;
Modern reporting uses report-to / Reporting API:
````
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'self'; report-to csp-endpoint;
Collect reports, fix violations, then switch to enforcing header. Violations in Report-Only do not block users—they are telemetry only.
Common breakages and fixes
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| All inline scripts blocked | No nonce/hash, no 'unsafe-inline' | Nonce pipeline or move to external files |
| Third-party widget fails | Domain not in script-src | Add host or load via trusted loader |
| API calls fail | connect-src too tight | Add API origin |
| WebSocket fails | Missing wss: in connect-src | Add WebSocket origin |
| Google Fonts broken | font-src / style-src | Add fonts.googleapis.com, fonts.gstatic.com |
| Dev hot reload broken | Local WS/http not allowed | Relax policy in dev only |
Keep development and production policies separate; document why dev exceptions exist.
CSP and XSS: what it does not fix
- Markup injection without script (defacement, phishing overlays) may still need sanitization
- JSONP and legacy gadgets if allowed by policy
- CSS injection can exfiltrate data in some browsers if
style-srcis too permissive - Browser extensions are outside your CSP
- Subresource Integrity (SRI) complements CSP for third-party scripts but does not replace it
CSP reduces impact of injection; it does not replace secure coding, output encoding, or authentication.
Combining with other headers
CSP works alongside:
X-Content-Type-Options: nosniffReferrer-PolicyPermissions-Policy(feature policy for camera, geolocation, etc.)Strict-Transport-Security(HSTS)
Use security header scanners as a checklist, not as proof of safety.
Framework and SPA considerations
Bundled SPAs (Vite, webpack): prefer single bundle from 'self'; avoid inline scripts. connect-src must include API hosts.
Server components / islands: nonces generated server-side per request.
Third-party tags (GTM, ads): often force permissive script-src—evaluate whether the business need justifies weakened policy; consider tag managers as supply chain risk.
Web Workers: may need worker-src or fall back to script-src / child-src depending on spec version.
Gradual enforcement checklist
- Inventory all script, style, font, image, connect, and frame sources on representative pages
- Deploy
Content-Security-Policy-Report-Onlywithreport-to - Triage reports for a soak period (days to weeks depending on traffic)
- Tighten directives; remove
'unsafe-inline'from scripts where possible - Switch to enforcing
Content-Security-Policy - Monitor reports and error tracking for new violations after deploys
Automate policy tests in CI where possible (e.g., crawl staging with headless browser and assert no console CSP errors on critical flows).
CSP Level 3 and trusted types
Modern CSP continues to evolve. Two related hardening ideas worth knowing:
require-trusted-types-for
When supported, this directive requires sinks like innerHTML and eval to accept only Trusted Types wrappers created by your policy code. It pairs with CSP to reduce DOM XSS sink abuse. Adoption is growing but not universal; feature-detect and fall back gracefully.
upgrade-insecure-requests
Automatically upgrades subresource requests from HTTP to HTTPS. Useful during migrations; less relevant if you already serve everything over HTTPS with HSTS.
Check MDN CSP reference and W3C TR for directive support in browsers you target—do not assume every directive works in every user agent.
Policy examples by application archetype
Marketing site (static, no user auth)
Tight script policy, images from CDN:
````
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.example.com data:;
connect-src 'self';
frame-ancestors 'none';
API-backed SPA
Scripts from app origin; API and WebSocket hosts in connect-src:
````
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.example.com wss://api.example.com;
img-src 'self' data: https:;
Admin panel with rich text editor
Editors often need 'unsafe-inline' for styles or blob workers—isolate admin on a separate origin or subdomain with a stricter public-site policy and a more tailored admin policy.
Document why each exception exists; review quarterly.
Limitations
CSP parsing and directive interaction are complex; subtle ordering and fallback rules confuse even experienced developers.
User agents differ slightly; test on browsers you support.
Overly long policies may hit header size limits on some proxies—split or simplify.
Meta-tag CSP cannot use all directives (e.g., frame-ancestors is ignored in meta).
Attackers with full server header control are outside the threat model—CSP protects users when injection is partial.
Summary
Content Security Policy tells browsers which resources may load and execute on your pages. Start with a restrictive baseline, use Report-Only to find breakages, adopt nonces or hashes instead of 'unsafe-inline' for scripts, and tune connect-src against data exfiltration. CSP is one layer in a secure web app—not a substitute for escaping untrusted data or reviewing third-party script supply chains.
