Content Security Policy (CSP) is one of the strongest browser defenses against XSS and unwanted script execution — and one of the easiest to misconfigure in a React or Vite SPA. A policy that is too loose fails open. A policy that is too tight whitescreens production because a hashed chunk, inline style, or analytics tag was never allowlisted. This guide focuses on SPA realities: bundlers, inline bootstraps, nonces, hashes, and how to roll out CSP without guessing.
Bottom line: Start with Report-Only, lock default-src and script-src first, prefer nonces or hashes over 'unsafe-inline', and expect Vite/React to need explicit allowances for modules and eval in development. Build headers with a deliberate tool, not copy-pasted Stack Overflow policies.
Who this guide is for
Frontend leads and platform engineers shipping SPAs (React, Vue, Svelte, Vite, CRA successors) behind CDNs or reverse proxies. If your app "works until CSP is enabled," use the break/fix sections below.
What CSP does
CSP tells the browser which origins and patterns may load scripts, styles, images, fonts, frames, and connections. It is delivered as an HTTP response header (preferred) or a <meta> http-equiv tag (more limited).
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
| Goal |
Typical directive |
| Stop random script injection |
script-src |
| Limit where data can be posted |
connect-src |
| Block plugin vectors |
object-src 'none' |
| Control framing / clickjacking |
frame-ancestors (header only) |
| Restrict form targets |
form-action |
CSP is not a substitute for output encoding or safe DOM APIs. It is defense in depth when XSS still slips through.
Core directives for SPAs
| Directive |
SPA guidance |
default-src 'self' |
Safe baseline; override per resource type |
script-src |
Strictest practical policy — nonces/hashes |
style-src |
'self' + hashes/nonces; avoid 'unsafe-inline' long-term |
img-src 'self' data: https: |
Tighten https: to known CDNs when possible |
font-src 'self' |
Add font CDN if used |
connect-src 'self' https://api.example.com |
Must include API, WebSocket, analytics endpoints |
frame-src / child-src |
Only payment/widgets you embed |
worker-src |
Needed for service workers / web workers |
object-src 'none' |
Almost always |
base-uri 'self' |
Mitigates <base> hijacks |
frame-ancestors 'none' or allowlist |
Clickjacking control (header, not meta) |
Build iteratively with the CSP builder so each directive is explicit and reviewable in PRs.
script-src: nonces, hashes, and 'strict-dynamic'
Why 'unsafe-inline' is the trap
Inline <script> tags and onclick= handlers are how XSS payloads run. Allowing 'unsafe-inline' in script-src largely defeats CSP for script injection. Many SPAs still ship with it because frameworks inject inline scripts during bootstrap.
Nonces
Server generates a random nonce per response; every legitimate inline script carries it:
<script nonce="r4nd0mN0nce">window.__ENV = { API: "https://api.example.com" };</script>
Content-Security-Policy: script-src 'nonce-r4nd0mN0nce' 'strict-dynamic'
Requirements:
- Nonce must be unpredictable and unique per response
- CDN/HTML caching must not serve a stale nonce to another user
- For pure static hosts (no per-request HTML), nonces are hard — use hashes or externalize scripts
Hashes
Hash the exact inline script body (SHA-256 / SHA-384 / SHA-512):
Content-Security-Policy: script-src 'self' 'sha256-AbCdEf...='
Any character change invalidates the hash. Good for small static inline bootstraps; painful if the inline script changes often.
'strict-dynamic'
When present, trust propagates from a nonce/hash-approved script to scripts it loads. Useful for modern SPAs that inject <script type="module"> tags. Older browsers need fallbacks — test your support matrix.
Common breaks with React and Vite
| Symptom |
Cause |
Fix |
Blank page, console CSP errors on eval |
Dev tooling / source maps / some libs use eval |
Relax only in development; avoid unsafe-eval in prod |
| Module scripts blocked |
Missing 'self' or CDN host in script-src |
Allow script origin; prefer type="module" + 'strict-dynamic' |
| Vite HMR WebSocket fails |
connect-src missing ws: / wss: / localhost |
Add dev-only connect-src for HMR |
| Inline style attributes blocked |
React style={{...}} → style="" |
style-src hashes/nonces or 'unsafe-inline' for styles (weaker) |
| Emotion / styled-components inject tags |
Runtime style insertion |
Nonce support in the CSS-in-JS library + CSP nonce |
| Google Fonts / analytics blocked |
Missing host in style-src / script-src / connect-src |
Allowlist exact hosts |
| JSON-LD or meta hydration script blocked |
Inline script without nonce/hash |
Move to external file or hash it |
Practical Vite + SPA pattern
Development: separate, looser policy or no CSP — HMR and tooling fight strict policies.
Production:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{PER_REQUEST}';
style-src 'self' 'nonce-{PER_REQUEST}';
img-src 'self' data: https://cdn.example.com;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
Serve HTML from an origin that can inject nonces (Edge worker, SSR adapter, or origin server). Fully static index.html on object storage often forces hash-based or looser policies — plan for that in architecture reviews.
CSP vs CORS (do not confuse them)
| Mechanism |
Protects |
Configured on |
| CSP |
Your page from loading bad resources / XSS impact |
Your document responses |
| CORS |
Other origins from reading your API responses in browsers |
API responses (Access-Control-*) |
A correct CSP does not replace CORS, and allowing an API in connect-src does not grant browsers cross-origin read access without CORS. When wiring APIs, generate headers carefully with the CORS header generator and keep CSP connect-src aligned with the same hosts.
CSP via <meta http-equiv="Content-Security-Policy" content="..."> works for many directives but cannot set frame-ancestors, report-uri/report-to in all cases the same way, and is easier to override inconsistently. Prefer HTTP headers at the CDN or app server. For general SEO/social meta (unrelated to CSP), the Meta tags generator helps with preview tags — keep security headers in infrastructure config.
Reporting and rollout
Report-Only first
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
Browsers enforce nothing; they send violation reports. Fix the noise, then switch to enforcing Content-Security-Policy.
What to collect
blocked-uri, effective-directive, document-uri
- Sample rate if volume is high
- Ignore known browser extensions that inject scripts (they create false positives)
Rollout checklist
- Inventory scripts, styles, fonts, APIs, websockets, frame embeds.
- Draft policy in the CSP builder.
- Deploy Report-Only to staging and production.
- Fix application code (remove drive-by inline scripts) before loosening policy.
- Enforce for a percentage of traffic if your edge supports it.
- Monitor error rates and blank-page reports for 48 hours.
- Document exceptions (payment iframes, chat widgets) with owners.
Dangerous shortcuts
| Shortcut |
Risk |
'unsafe-inline' on script-src |
XSS executes freely |
'unsafe-eval' in production |
Enables eval-based XSS gadgets |
* or https: in script-src |
Any HTTPS script can run |
| Copying a "strict" policy without testing |
Production outage |
| Putting secrets in CSP reports endpoints without auth |
Report spam / data leak |
Practical takeaway
CSP for SPAs succeeds when architecture supports per-response nonces or stable hashes, and when connect-src matches real APIs. Use Report-Only to learn; enforce only after React/Vite assets, styles, and third parties are accounted for. Draft policies with the CSP builder, align API access with the CORS header generator, and keep marketing/SEO meta separate via the Meta tags generator. Strict script-src is the directive that pays the rent — protect it.
Sources