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.coproject host (andwss://for Realtime). Do not copy someone else's project ref. - Stripe.js loads from
js.stripe.comand talks toapi.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-ancestorsin 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>inindex.html(analytics snippets, theme bootstraps) dangerouslySetInnerHTMLthat injects scripts- Eval-based libraries (
script-srcneeding'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:
- Move snippets into
.jsfiles served from'self'or a known CDN host. - 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. - Prefer
'unsafe-inline'only onstyle-srcif your CSS-in-JS injects style tags; try to remove it fromscript-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:
- Rename
Content-Security-Policy-Report-Only→Content-Security-Policywith the same value. - Redeploy and re-click critical paths.
- Keep a report endpoint or periodic
curl -Iin 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
- Ship Report-Only with
default-src 'self'and tightscript-src/connect-src. - Click every auth, upload and payment path; paste violation hosts into the policy.
- Remove leftover inline scripts from
index.htmlinstead of adding'unsafe-inline'toscript-src. - Flip to enforcing CSP; keep HSTS,
X-Frame-Options/frame-ancestors, andnosnifffrom your host header pack. - Re-check with
curl -Iafter each host or CDN change so a stale_headerspath 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.
Related reading
- Missing security headers guide
- Add security headers on Vercel
- Add security headers on Netlify
- Is Access-Control-Allow-Origin: * safe?
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.
Fix guides for this topic
Platform checklists: Bolt · Lovable · Vercel