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-endpoint

Pair 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 to addEventListener calls in nonced or external scripts.

  • Inline <style> blocks and style="..." attributes. CSP allows nonces on <style> elements but not on style attributes. Either accept 'unsafe-inline' in style-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-endpoint

Why 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 in script-src except 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' and https: 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 blocks javascript: 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' and frame-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; add connect-src https://api-iam.intercom.io wss://nexus-websocket-a.intercom.io and img-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' plus connect-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.