How do I add a Content Security Policy without breaking my app?
Inventory what the app loads, start the policy in report-only mode, collect real violations safely, then enforce it once a week of traffic is clean.
The problem
For anyone whose security scan says "missing Content-Security-Policy", or who added one and watched payments, analytics or fonts stop working. AI tools rarely add a CSP, and a guessed one breaks things silently.
How to use it
- Download the file (or copy it).
- Paste it into your AI tool: Cursor, Claude Code or Lovable.
- Follow the checks it lists, one by one.
Context for the AI tool
You are adding a Content Security Policy (CSP) to this app without breaking any feature.
- Do not ship an enforcing policy. Start with
Content-Security-Policy-Report-Onlyand keep it that way until I confirm, after at least a week of real traffic without unexplained violations. - Before writing the policy, list every external origin the app loads from and report it as a table: origin, what loads from it (script, style, font, image, frame, connection), which feature needs it.
- Never add
'unsafe-eval', and never allow*or a whole scheme such ashttps:to make a violation go away. Ask me instead. - Do not log full URLs from violation reports. Log the directive, the blocked origin and the page path only: URLs can contain visitor data in query strings.
1. List what the app loads
What to look for: every origin the browser contacts. Open each main page (home, sign-up,
log-in, the main screen, checkout or payment) with the browser's Network tab open, and sort by
domain. Typical entries: your own API and storage (for example <project>.supabase.co), payment
(js.stripe.com), analytics and ads (Google Tag Manager, Google Analytics, Meta), fonts
(fonts.googleapis.com, fonts.gstatic.com), embeds (YouTube, maps, Calendly), chat widgets.
grep -rhoE "https://[a-zA-Z0-9.-]+" --include=*.{ts,tsx,js,jsx,svelte,vue,html} src app pages components 2>/dev/null | sort -u
How to fix: nothing yet. Combine the Network tab and the grep into the origin table.
How to verify: every row has a feature next to it. Origins nobody can explain are removed from the code, not added to the policy.
2. Write a report-only policy
What to look for: where the app sets HTTP headers: the framework (next.config.js
headers(), SvelteKit kit.csp or a handle hook), or the host (vercel.json, _headers on
Netlify, customHttp.yml on AWS Amplify).
How to fix: set one Content-Security-Policy-Report-Only header built from the table. A
starting point to adjust:
default-src 'self';
script-src 'self' https://js.stripe.com https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://<project>.supabase.co wss://<project>.supabase.co;
frame-src https://js.stripe.com;
object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';
report-uri /csp-report
If the framework can add nonces or hashes for its own inline scripts (SvelteKit kit.csp,
Next.js middleware with a nonce), use that instead of 'unsafe-inline' in script-src.
How to verify: the header is present on every page (curl -sI https://<your-app>/ | grep -i content-security)
and the app still works exactly as before, because report-only blocks nothing.
3. Collect violations from real visitors
What to look for: an endpoint for report-uri (and optionally report-to) that stores the
reports somewhere you will actually read.
How to fix: add a small POST endpoint, for example /csp-report, that accepts both formats
(report-uri sends {"csp-report": {...}}, the Reporting API sends an array of
{type, body}), logs one line per violation with directive, blocked origin and page path, and
returns 204. Limit the body size and don't require authentication on it.
How to verify: trigger a violation yourself (load a page with a test image from an origin that is not allowed) and find the log line. Check the logs really are kept: on some hosts the server function has no log storage until you turn it on, and then every report disappears.
4. Read a week of reports
What to look for: each violation that comes from a real feature. A common one: an ad or analytics tag that pings a second host only after a conversion, so it never shows up when you browse the site yourself.
How to fix: add the origin to the narrowest directive that needs it, with a note saying
which feature it is for. Ignore noise from browser extensions (chrome-extension://,
moz-extension://) and translation tools.
How to verify: after a full week, including a real purchase or sign-up, no violations remain that belong to a feature.
5. Enforce
How to fix: rename the header to Content-Security-Policy and keep report-uri, so new
violations still reach you after a third-party script changes.
How to verify: run through sign-up, log-in, payment, analytics and every embed in a private window with the console open: no CSP errors. Check that your analytics and ad conversions keep arriving for the next days. A free scan on securityheaders.com shows the header as present.
Did this fix it?
Email me new fixes
One short email when we publish a new fix. Nothing else, and you can unsubscribe anytime.
Still stuck? Book a call
A WDDW developer looks at your app with you. Free 15-minute call, no strings.