We Do Dev Work
We Do Dev Work
Next.jsSvelteKitVercelNetlifyAWS AmplifyLovableCursorClaude Code

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

  1. Download the file (or copy it).
  2. Paste it into your AI tool: Cursor, Claude Code or Lovable.
  3. 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-Only and 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 as https: 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.

All free fixes

Got a dev problem?

You don't need to know exactly what's wrong. That's kind of the point.

Send the signal