How to Roll Out a Content Security Policy Without Breaking Your Site
JUL 13, 2026 - Written by Yves SoeteBlacksight LLC — free website security scanner atscanner.blacksight.io
Deploy CSP in stages: inventory every domain your site loads content from, ship the policy as Content-Security-Policy-Report-Only, watch violation reports for one to two weeks, replace unsafe-inline with nonces or hashes, then switch to enforcement. Teams that skip the Report-Only stage either break their own checkout flow on day one or ship a policy so permissive it stops nothing.
Content-Security-Policy has a reputation problem. Everyone agrees it is the strongest browser-side defense against cross-site scripting, and almost nobody runs it in a state that actually defends anything. In our security headers guide we covered what CSP is; this post is the missing operational half — how to get from "no policy" to "enforced policy" without a 2 a.m. rollback.
What does CSP actually stop?
CSP is an allowlist the browser enforces. You declare which origins may supply scripts, styles, images, fonts, frames, and network destinations; the browser refuses everything else. That blocks the payload stage of most XSS — the injected script tag pointing at an attacker's server never executes. It blocks Magecart-style skimmers that load from a domain you never approved. Through form-action and connect-src it constrains where captured data can even be sent, and frame-ancestors is the modern anti-clickjacking control.
CSP is a last line of defense, not a replacement for output encoding and input validation. But when your application code fails — or a third party you trusted gets compromised — it is the difference between a violation report and a breach disclosure.
Why do most CSP deployments protect nothing?
Because they were written to stop the console warnings, not the attacker. The pattern is always the same: someone deploys a strict policy, the site visibly breaks, and the fix applied under pressure is
unsafe-inline
in script-src — which re-allows exactly the thing XSS injects — or a wildcard source, which allows everyone. The policy now passes every compliance checkbox and stops nothing.
The other failure mode is staleness. A policy written in January does not know about the chat widget marketing added in March. Either the widget breaks (and someone neuters the policy) or the policy was already permissive enough not to notice (and it was never protecting you). A CSP is not a header you set; it is a small process you run. Here is that process.
Step 1: Inventory every content source
Open DevTools, Network tab, reload your key pages — home, login, checkout, anything with embeds — and write down every domain that serves scripts, styles, fonts, images, and XHR/fetch requests. Do this on pages with consent banners accepted, because tag managers load half their payload only after consent.
Manual inventory misses things that load conditionally, which is why we built the
supply-chain scanner
to enumerate every third-party script your site actually loads — the same inventory you need for
auditing third-party scripts
in general. Expect surprises; most sites load scripts nobody currently on the team can explain.
Step 2: Ship it as Report-Only
Write the strictest policy your inventory supports and deploy it under the report-only header name. Nothing is blocked in this mode — the browser just tells you what would have been.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; report-uri /csp-violations
The report-uri directive is deprecated in favor of report-to, but it still works everywhere and is simpler to stand up — set both if you want to be current. The endpoint receives a JSON body per violation; log them somewhere you can aggregate by directive and blocked domain. If you do not want to host an endpoint, several hosted CSP report collectors exist, or start by just watching the browser console on your own sessions.
Step 3: Triage reports for one to two weeks
Give the report-only phase at least one full deploy cycle and, ideally, one marketing campaign. The reports split into three buckets. Legitimate sources you forgot — a CDN domain, a captcha, an A/B testing tool — get added to the policy. Noise from browser extensions — violations citing extension URLs or injected inline scripts on a handful of clients — gets ignored; you cannot and should not try to allowlist users' extensions. The third bucket is the interesting one: sources nobody recognizes. Investigate those before you allowlist anything, because "unknown script the policy would have blocked" is either an unowned dependency or your first real catch.
Step 4: Replace unsafe-inline with nonces or hashes
If your own templates use inline script blocks, the fix that preserves CSP's value is a per-request nonce: generate a random value on every response, stamp it on your script tags, and reference it in the policy.
script-src 'self' 'nonce-<random-per-request-value>' 'strict-dynamic'
The nonce must be unpredictable and fresh per response — a static string defeats the mechanism entirely. For inline snippets that never change, a hash source (sha256 of the exact script text) works without any server-side generation. And
strict-dynamic
solves the tag-manager problem: scripts loaded by your nonce-approved script are trusted transitively, so Google Tag Manager can keep injecting its payloads without you allowlisting every downstream domain — while unapproved injected scripts still get blocked.
Two pragmatic notes. Keeping 'unsafe-inline'
in style-src is a common and defensible first-pass compromise — inline styles are a far smaller attack surface than inline scripts. And event-handler attributes like onclick in old templates will violate script-src regardless of nonces; migrating them to addEventListener is usually the single largest chunk of real work in a CSP rollout. Budget for it.
Step 5: Enforce, and keep it alive
When a week of reports shows only extension noise, rename the header.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<random-per-request-value>' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; report-uri /csp-violations
Keep the reporting endpoint forever — in enforcement mode it is your early-warning system. A spike in violations after a deploy means you broke something; a spike without a deploy means a third party changed under you, which is exactly the scenario SRI exists for. CSP and SRI are complements: CSP controls which origins may serve code, SRI pins the exact bytes. Use both on anything payment-adjacent.
Finally, re-verify from the outside after every infrastructure change — CDNs, reverse proxies, and load balancers can strip or override headers, so what your application sends is not always what browsers receive. BlackSight's headers scan checks the CSP a visitor actually gets, flags unsafe-inline and wildcard sources, and re-checks on every scheduled scan, which is the cheapest way to notice your policy silently regressed.
The short version
Inventory, Report-Only, triage, nonces, enforce — in that order, with no stage skipped. Rolled out this way, CSP is not the site-breaking monster of its reputation; it is a week or two of attentive work that permanently raises the cost of every XSS and supply-chain attack against your site.
Want to know what your site sends today? Run a free scan at
scanner.blacksight.io
— the headers report will show your CSP, or the absence of one, in about 30 seconds.
Bonus: our free scanner also audits SSL, cookies, and third-party scripts at
scanner.blacksight.io
Liked this article? Get notified when new articles drop — visitblacksight.io/blogto subscribe.