Most CSP rollouts fail one of two ways. The team ships a strict policy on Friday, the analytics tag breaks on Saturday, and on Monday someone adds 'unsafe-inline' to make the alarms stop. The header is still there. The XSS protection is gone. Google's research on real-world CSP deployments found this pattern is the norm — the majority of policies they evaluated were trivially bypassable, mostly because of 'unsafe-inline' or overly broad host allowlists. A CSP that exists on paper does nothing for users.
The pragmatic path is a phased rollout: collect real violations against your live traffic, refactor the inline handlers a strict policy would block, then enforce a 'strict-dynamic' policy that survives third-party scripts loading other scripts. Below is the migration we recommend.
Phase 1: report-only against production
Ship Content-Security-Policy-Report-Only first. Browsers evaluate the policy and send violation reports without blocking anything. Synthetic test environments lie about what runs on your site — the long tail of legacy inline handlers, vendor scripts, A/B test snippets, and browser extensions only shows up against real users.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-{RANDOM_PER_REQUEST}'; object-src 'none'; base-uri 'none'; report-to csp-endpointPair the header with a reporting endpoint. The legacy report-uri directive POSTs JSON to a URL of your choice and is supported in every browser that supports CSP. The modern report-to directive uses the Reporting API, which Chromium browsers (Chrome 69+, Edge 79+) support fully. Firefox added report-to parsing in version 93 but its Reporting API support remains partial, and Safari still favors report-uri. Send both. Browsers that understand report-to ignore report-uri; the rest fall back.
Run report-only for at least two weeks. Long enough to catch the weekly cron job, the monthly marketing campaign, and the user on the corporate proxy that injects an enterprise toolbar. Bucket reports by blocked-uri and violated-directive. Real signals will surface fast; the noise is mostly browser extensions, which you cannot fix and should filter out.
Phase 2: refactor the breakage you can fix
Three categories cause most of the violations you'll see:
Inline event handlers (
onclick="...",onerror="...") injected by old templates or rich-text editors. A strict CSP blocks these unconditionally — there is no nonce mechanism for inline handlers in CSP3. Move them toaddEventListenercalls in nonced or external scripts.Inline
<style>blocks andstyle="..."attributes. CSP allows nonces on<style>elements but not onstyleattributes. Either accept'unsafe-inline'instyle-src(lower XSS impact than scripts), or migrate to classes. We recommend the former unless you have a specific reason; style-based attacks are rare and the migration cost is high.Tag managers and vendor snippets that inject other scripts at runtime. Google Tag Manager, Segment, Intercom, and similar all need
'strict-dynamic'or an explicit allowlist.'strict-dynamic'is the right answer for almost every case.
'strict-dynamic' changes the trust model. When a script with a valid nonce or hash loads another script, that loaded script inherits trust. You nonce one bootstrap loader; everything it pulls in runs without per-request nonce coordination. This is what makes shipping CSP on a site with vendor scripts realistic.
Phase 3: the policy you actually enforce
Switch the header from Content-Security-Policy-Report-Only to Content-Security-Policy once your violation rate is dominated by extensions and known-third-party noise. The recommended starting policy:
Content-Security-Policy: default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-{RANDOM_PER_REQUEST}' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; report-to csp-endpointWhy every keyword is in there:
'strict-dynamic'is the actual XSS protection. Modern browsers (Chrome 52+, Edge 79+, Firefox 52+, Safari 15.4+) honor it and ignore everything else inscript-srcexcept the nonce.'nonce-{RANDOM}'must be a fresh cryptographically random value per response, attached to every first-party<script>tag your server renders. 128 bits is the minimum.'unsafe-inline'andhttps:exist for browsers that don't understand'strict-dynamic'. Modern browsers ignore both when a nonce or'strict-dynamic'is present, so they don't weaken the strong policy. Older browsers fall back to host-based protection, which is weaker but still blocksjavascript:URIs and a chunk of injected<script src="">cases.base-uri 'none'blocks<base href="">injection, a real attack vector that bypasses many otherwise-strict policies.object-src 'none'andframe-ancestors 'none'close the Flash and clickjacking surfaces.
Third-party services and the directives they need
The vendor docs are usually wrong or out of date. Patterns that work in production:
Google Tag Manager / GA4: covered by
'strict-dynamic'if you nonce the GTM bootstrap snippet. No host allowlist needed.Stripe:
script-src https://js.stripe.com,frame-src https://js.stripe.com https://hooks.stripe.com,connect-src https://api.stripe.com.Intercom:
'strict-dynamic'covers script loading; addconnect-src https://api-iam.intercom.io wss://nexus-websocket-a.intercom.ioandimg-src https://js.intercomcdn.com.Cloudflare Turnstile:
script-src https://challenges.cloudflare.com,frame-src https://challenges.cloudflare.com.Sentry browser SDK:
connect-src https://*.ingest.sentry.io(or your self-hosted DSN host).Segment:
'strict-dynamic'plusconnect-src https://api.segment.io https://cdn.segment.com.
If you're not using 'strict-dynamic', every one of these turns into an allowlist of CDN hosts, several of which (Google's hosted libraries, common JSONP endpoints) carry documented CSP bypasses.
Verify the policy is actually strict
Paste your enforced header into Google's CSP Evaluator before you ship. It flags the bypass-prone directives — 'unsafe-eval', missing object-src, missing base-uri, host allowlists with known JSONP endpoints — and grades each one. A policy that scores green on the evaluator and serves real traffic in report-only with low violation rates is ready to enforce.
The CSP Level 3 spec (W3C Working Draft, 21 April 2026) is what modern browsers implement. Older guides referencing CSP2 patterns will steer you toward host allowlists; the strict-dynamic flow above is what the spec authors and Google's security team recommend now.
Verify before shipping
isitready.dev parses your live Content-Security-Policy header, runs it through the same evaluator logic, and flags 'unsafe-inline' without a nonce, missing object-src, missing base-uri, and other bypass-prone patterns alongside the rest of your security header baseline. Run it against the canonical production origin after the report-only window closes and again after you flip to enforce.