Skip to main content
{/}codefunc
Web & APIJuly 12, 2026 · 6 min read

Content Security Policy for SPAs: Directives, Nonces, and Fixing React/Vite Breakage

Configure CSP for single-page apps — script-src with nonces and hashes, common React and Vite failures, CORS interplay, reporting, and a practical rollout checklist.

By codefunc

cspsecurityspaxssheaders

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.

Meta tags vs HTTP headers

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

  1. Inventory scripts, styles, fonts, APIs, websockets, frame embeds.
  2. Draft policy in the CSP builder.
  3. Deploy Report-Only to staging and production.
  4. Fix application code (remove drive-by inline scripts) before loosening policy.
  5. Enforce for a percentage of traffic if your edge supports it.
  6. Monitor error rates and blank-page reports for 48 hours.
  7. 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

Try it on codefunc

Related articles

3

Search tools

Find a developer tool