Guide
Content Security Policy (CSP) explained: headers and XSS defense
Cross-site scripting (XSS) lets attackers run JavaScript in your users' browsers — stealing session cookies, rewriting wallet prompts, or exfiltrating API keys. Input sanitization helps, but it is easy to miss an edge case. Content Security Policy (CSP) is a browser-enforced allowlist: you declare which origins may load scripts, styles, images, and network connections. Everything else is blocked before it executes. For publishers, SaaS dashboards, and wallet-connected dApps, a well-tuned CSP is one of the highest-leverage security headers you can add.
How CSP works
The server sends a Content-Security-Policy HTTP response header
(or a <meta http-equiv="Content-Security-Policy"> tag, though
headers are preferred). The browser parses a list of directives,
each naming a resource type and an allowlist of sources.
Example minimal policy:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
Here, all resources default to same-origin only. Scripts must come from your
domain. Embedded plugins (object, embed) are disabled.
base-uri stops injected <base href> tags from
hijacking relative URLs — a subtle XSS amplifier.
When a page violates the policy, modern browsers block the resource and log a console error. In report-only mode (see below), violations are reported but not blocked — useful during rollout.
Directives you will actually use
CSP has dozens of directives; most apps need a focused subset:
default-src— fallback when a more specific directive is absent. Start restrictive:'self'.script-src— the heart of XSS defense. Controls<script>, inline handlers, andeval(). This is where you spend most tuning time.style-src— CSS sources. Inlinestyle=""attributes and<style>blocks need explicit permission or hashes.img-src— images and favicons. Often'self' data: https:for CDNs and data-URI avatars.connect-src—fetch,XMLHttpRequest, WebSockets, andEventSource. Critical for SPAs calling APIs and dApps hitting RPC endpoints.font-src— web fonts (Google Fonts, self-hosted).frame-src/child-src— embedded iframes (payment widgets, OAuth popups).frame-ancestors— who may embed your page in an iframe. Replaces the olderX-Frame-Optionsheader. Use'none'or an explicit parent list to block clickjacking.form-action— where HTML forms may submit. Stops injected forms from posting credentials to attacker domains.upgrade-insecure-requests— rewriteshttp://subresource URLs to HTTPS. Good default on production sites.
Source expressions
Each directive accepts space-separated source expressions:
'self'— same scheme, host, and port as the documenthttps://cdn.example.com— exact origin allowlisthttps:— any HTTPS origin (broad; use sparingly)'none'— block everything for that directive'unsafe-inline'— allow inline scripts/styles (defeats much of CSP's value — avoid if possible)'unsafe-eval'— alloweval()andnew Function()(needed by some bundlers/dev tools; remove in production if you can)'nonce-abc123'— allow inline scripts/styles tagged with a matching nonce attribute'sha256-…'— allow a specific inline block by its hash
Stopping XSS: script-src strategies
The classic XSS pattern injects <script>stealCookies()</script>
or an event handler like onerror= on an image tag. A strict
script-src blocks unknown external scripts and, without
'unsafe-inline', blocks inline script too.
Nonce-based CSP (recommended for dynamic sites)
Your server generates a random nonce per request, includes it in
the header and on each legitimate <script> tag:
Content-Security-Policy: script-src 'nonce-R4nd0mPerRequest'
<script nonce="R4nd0mPerRequest" src="/app.js"></script>
Injected scripts lack the nonce and are blocked. Frameworks like Next.js, Rails,
and nginx with sub_filter can inject nonces automatically. The nonce
must be unpredictable and single-use per response — never hard-code it.
Hash-based CSP (good for static sites)
For a small set of inline scripts, compute a SHA-256 hash of the exact script body and whitelist it:
script-src 'sha256-9d4Czl0…==' 'self'
Any change to the script body changes the hash — fine for immutable build artifacts, painful for frequently edited inline snippets.
Strict CSP without inline script at all
The gold standard: script-src 'self' with all JavaScript in
external files, no inline handlers, no javascript: URLs. Static
sites and well-structured SPAs can achieve this. Third-party widgets (analytics,
AdSense, wallet SDKs) must be explicitly listed by origin.
Report-only mode and monitoring
Rolling out CSP on a legacy app breaks things. Use
Content-Security-Policy-Report-Only first — same syntax, but
violations are reported instead of blocked. Pair with
report-uri (legacy) or report-to (modern) to send
JSON violation reports to your logging endpoint.
Typical rollout:
- Deploy report-only policy, collect a week of violation logs.
- Fix false positives — add missing CDN origins, migrate inline scripts to files.
- Switch to enforcing
Content-Security-Policywith the same rules. - Keep
report-toon the enforcing policy for ongoing monitoring.
Browser DevTools also surfaces violations in the Console tab — filter for "Content Security Policy" when debugging locally.
CSP for SPAs, APIs, and crypto dApps
Single-page apps and wallet-connected sites have extra connect-src
requirements. Your policy must allow:
- Your own API origin (
'self'orhttps://api.example.com) - Third-party RPC providers (e.g.
https://mainnet.helius-rpc.com) — list each host explicitly rather than wildcarding all HTTPS - WebSocket endpoints if you use real-time feeds
- Analytics and ad networks if you run them — Google AdSense requires
https://pagead2.googlesyndication.cominscript-srcand related ad domains inframe-src/img-src
Wallet browser extensions inject content scripts into your page. CSP does not block extension-injected scripts — extensions operate outside the page CSP. Your policy still protects against XSS that runs in the page context without extension involvement.
If you proxy RPC through your backend (recommended to hide API keys and avoid
browser-side rate limits), tighten connect-src to your proxy only
instead of exposing every RPC hostname to the client. See our
Solana RPC endpoints guide
for why that pattern matters.
CSP vs other security headers
CSP complements — does not replace — other defenses:
- CORS controls whether other origins can read your API responses. CSP controls what your page can load. You need both. See CORS explained.
- HttpOnly cookies keep session tokens out of JavaScript reach. CSP reduces the chance a stolen inline script runs at all.
- Subresource Integrity (SRI) hashes external script files so a compromised CDN cannot swap code. Pair SRI with a strict
script-src. Referrer-PolicyandPermissions-Policylimit metadata leakage and browser feature access (camera, geolocation).
For authenticated APIs, combine CSP on the frontend with short-lived JWT or OAuth tokens on the backend — defense in depth, not either/or.
Common mistakes and how to fix them
| Symptom | Likely cause | Fix |
|---|---|---|
| Inline script blocked | No nonce/hash and no 'unsafe-inline' |
Move JS to external file, or add per-request nonce |
| Google Fonts CSS blocked | style-src too strict |
Add https://fonts.googleapis.com to style-src; https://fonts.gstatic.com to font-src |
| API fetch fails silently | Missing connect-src entry |
Add API and RPC origins explicitly; check DevTools Console |
| Site works in Chrome, breaks in Safari | Browser-specific directive support | Test both; avoid experimental directives in production |
| Policy seems ignored | Meta tag cannot set report-uri or frame-ancestors |
Send CSP via HTTP header from nginx/Cloudflare instead |
Avoid script-src * or blanket unsafe-inline unsafe-eval
on production — scanners flag them, and they negate most XSS protection.
Example policies by site type
Static content site (no third-party scripts)
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
Site with analytics and ads
Ad networks need explicit script and frame origins. Start from the vendor's
documented CSP requirements, deploy report-only, then enforce. Allow
https://pagead2.googlesyndication.com in script-src for
AdSense; add their image and frame domains per Google's publisher documentation.
Solana dApp with backend RPC proxy
Content-Security-Policy:
default-src 'self';
script-src 'self';
connect-src 'self' wss://your-api.example.com;
img-src 'self' data: https:;
frame-ancestors 'none'
Wallet popups and QR flows typically do not require widening frame-src
on your origin — the wallet opens in extension UI or a separate app, not an
iframe you embed.
Checklist before you ship
- Send CSP via HTTP header, not only a meta tag.
- Run report-only in staging; grep logs for unexpected blocked URIs.
- List every third-party script, font, API, and RPC host explicitly.
- Set
object-src 'none'andbase-uri 'self'. - Set
frame-ancestorsto block clickjacking. - Remove
'unsafe-eval'in production unless a dependency requires it. - Re-test after each new widget, A/B snippet, or CDN migration.
- Document the policy in your repo so the next deploy does not silently widen it.
CSP is not a silver bullet — DOM-based XSS, compromised dependencies, and social engineering still happen. But a strict policy turns many injection bugs from "full account takeover" into "console error, attack blocked" — which is exactly the margin you want on a public-facing product.
Related reading
- CORS explained — same-origin policy for API responses vs CSP for page resources
- JWT explained — token validation alongside frontend hardening
- HTTP caching explained — cache headers that pair with security headers on static assets
- All Solana Garden guides