Skip to content
StackSiren

How do I add a Content-Security-Policy to a React + Vite app?

By StackSiren · Published

Start with Content-Security-Policy-Report-Only, open the browser console on every important page, add the hosts and script sources the violations name, then flip the same policy to enforcing Content-Security-Policy.

CSP is an HTTP response header (or meta tag, with limits). It tells the browser which scripts, styles, images and connections your page may use. Vite + React apps usually need allowances for your own origin, any CDN, and backends such as Supabase or Stripe.js. Pair this with the broader security headers guide and host recipes for Vercel and Netlify.

Report-only first

Enforcing a tight CSP on day one will blank your app. MDN documents Content-Security-Policy-Report-Only as the safe rehearsal: the browser logs violations and can POST reports, but does not block.

Vercel (vercel.json):

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy-Report-Only",
          "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
        }
      ]
    }
  ]
}

Netlify (_headers in the publish directory):

/*
  Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

Deploy, click through login, dashboards and checkout, and watch the console for Refused to … because it violates the following Content Security Policy directive. Each message names the directive and the blocked URL — that list becomes your allowlist.

Optional: add report-uri / report-to when you have an endpoint that stores reports. Until then, the console is enough.

Directives for Supabase and Stripe

Most vibe-coded apps need connect-src (XHR/fetch/WebSocket) and sometimes frame-src / script-src for payment widgets.

Example once you know your project ref (replace the hostnames you actually use):

default-src 'self';
script-src 'self' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self'
  https://YOUR-PROJECT.supabase.co
  wss://YOUR-PROJECT.supabase.co
  https://api.stripe.com;
frame-src https://js.stripe.com https://hooks.stripe.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requests

Notes:

  • Supabase Auth, PostgREST, Storage and Realtime all go through your *.supabase.co project host (and wss:// for Realtime). Do not copy someone else's project ref.
  • Stripe.js loads from js.stripe.com and talks to api.stripe.com; Elements may frame Stripe hosts. Confirm against Stripe's current CSP docs if you use Checkout or Connect — hosts change by product.
  • img-src https: is a broad starter for user avatars; tighten to your CDN when you can.
  • Keep frame-ancestors in the header, not a meta tag — MDN notes that directive is ignored in <meta http-equiv>.

If you also use a wildcard CORS setup on APIs, fix that separately: Is Access-Control-Allow-Origin: * safe?.

Inline scripts and Vite

Vite's production build emits hashed JS modules under /assets/. A script-src 'self' policy usually works after you remove inline script tags and onclick= handlers.

What breaks CSP:

  • Inline <script>…</script> in index.html (analytics snippets, theme bootstraps)
  • dangerouslySetInnerHTML that injects scripts
  • Eval-based libraries (script-src needing 'unsafe-eval' — avoid if you can)
  • Dev-only: Vite's HMR client. Prefer testing CSP on a production preview deploy, not vite dev

Mitigations MDN lists:

  1. Move snippets into .js files served from 'self' or a known CDN host.
  2. Use a nonce (script-src 'nonce-…') or hash ('sha256-…') for the few inline scripts you must keep. Nonces need the server to mint a fresh value per response — static Vite hosts often prefer hashes or external files.
  3. Prefer 'unsafe-inline' only on style-src if your CSS-in-JS injects style tags; try to remove it from script-src.

Hosts: where to set the header

Host Mechanism Notes
Vercel / v0 vercel.json headers or next.config.js headers() See Vercel security headers
Netlify / many Bolt exports _headers or netlify.toml [[headers]] See Netlify security headers
Cloudflare / other CDN Transform / response header rules Useful when the app host cannot edit headers (some managed builders)
Meta tag <meta http-equiv="Content-Security-Policy" …> Incomplete: cannot set frame-ancestors; prefer HTTP header

Confirm after deploy:

curl -sI https://yourapp.com/ | grep -i content-security-policy

You want either the Report-Only header (while rehearsing) or the enforcing header (after violations are quiet) — not neither.

Enforce, then monitor

When the console is clean on production flows:

  1. Rename Content-Security-Policy-Report-Only → Content-Security-Policy with the same value.
  2. Redeploy and re-click critical paths.
  3. Keep a report endpoint or periodic curl -I in CI so a host change cannot drop the header silently.

StackSiren's free first scan of a site you own flags missing security headers; use the security headers checker for a focused pass. CSP will still need the console-driven allowlist above — scanners cannot invent your Stripe or Supabase hosts for you.

A minimal hardening order

  1. Ship Report-Only with default-src 'self' and tight script-src / connect-src.
  2. Click every auth, upload and payment path; paste violation hosts into the policy.
  3. Remove leftover inline scripts from index.html instead of adding 'unsafe-inline' to script-src.
  4. Flip to enforcing CSP; keep HSTS, X-Frame-Options / frame-ancestors, and nosniff from your host header pack.
  5. Re-check with curl -I after each host or CDN change so a stale _headers path cannot drop the policy.

Bolt and Lovable projects that deploy to Netlify or Vercel follow the same header files — only the project hosts in connect-src change. If a managed host will not let you set headers, put the CSP on a CDN in front of the custom domain rather than inventing a fake in-app setting.

FAQ

Can I put CSP only in index.html as a meta tag? Partly. Several directives work; frame-ancestors does not. Prefer an HTTP header from your host.

Why does my app work locally but break with CSP on Vercel? Local vite HMR and localhost hosts differ from production asset URLs. Rehearse Report-Only on a preview deployment.

Do I need 'unsafe-inline' for React? Not for scripts if you avoid inline tags. Many CSS-in-JS setups still want 'unsafe-inline' on style-src unless you adopt nonces/hashes for styles.

Does CSP replace XSS-free coding? No. It reduces the blast radius of an injection. Still escape user HTML and avoid eval.

What about report-uri vs report-to? Both are documented on MDN. report-to is the newer Reporting API path; report-uri is widely supported. Either is optional while you use the console.

How do I handle multiple environments? Use separate allowlists per environment (staging project refs, test Stripe keys' hosts are the same, but preview URLs differ). Do not ship a policy that allows every *.vercel.app preview if you can avoid it.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Bolt · Lovable · Vercel

Related posts

Sources