Every engineering team eventually receives a compliance ticket: "Missing Content-Security-Policy HTTP header."
On paper, CSP looks simple: instruct the browser which origins can execute scripts, load stylesheets, or open network connections. You draft a basic policy, test your local build, and deploy.
Then production breaks.
Dynamic script chunks fail with Refused to execute inline script, telemetry streams to Sentry drop to zero, and third-party widgets fail to load. Modern frontend architectures rely heavily on code-splitting, runtime CSS injection, and client-side hydration.
Here are 5 common CSP edge cases that break production applications and how to resolve them.
1. The 'unsafe-inline' and Nonce Fallback Trap
When migrating legacy applications to strict CSP, developers often combine nonces with 'unsafe-inline' for backward compatibility:
Content-Security-Policy: script-src 'nonce-r4nd0mStr1ng' 'unsafe-inline' https:;
Under CSP Level 2 and Level 3 specs, modern browsers completely ignore 'unsafe-inline' whenever a nonce or hash is present.
If a third-party script or library creates a dynamic tag (document.createElement('script')) without attaching the active nonce attribute (script.nonce = '...'), modern browsers immediately block it. Because 'unsafe-inline' is disabled by the presence of the nonce, the script will not execute.
Fix: Ensure your bundler or script loader propagates the server nonce to all dynamically injected tags, or adopt 'strict-dynamic'.
2. The 'strict-dynamic' Allowlist Override
CSP Level 3 introduced 'strict-dynamic' to streamline script loading in Single Page Applications:
Content-Security-Policy: script-src 'nonce-r4nd0mStr1ng' 'strict-dynamic' https://apis.google.com;
With 'strict-dynamic', trust propagates: any script authorized by a valid nonce can dynamically append new <script> tags, and the browser trusts them automatically.
However, 'strict-dynamic' disables all host-based allowlists and 'self'. In the policy above, https://apis.google.com is ignored by CSP Level 3 browsers. If your HTML contains an external static tag <script src="https://apis.google.com/js/api.js"></script> without a nonce attribute, it gets blocked despite the domain being listed.
Rule: Under 'strict-dynamic', every parser-inserted <script> in your initial HTML must have a valid nonce.
3. Meta Tag Directives That Silently Do Nothing
For static client SPAs hosted on S3 or GitHub Pages, developers often place their CSP inside an HTML <meta> tag:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';">
While <meta> tags handle basic source controls, W3C specifications explicitly disallow key directives inside <meta> tags:
-
frame-ancestors(essential for preventing clickjacking) -
report-toandreport-uri(blocking violation monitoring) sandbox
Additionally, <meta> tags cannot be used with Content-Security-Policy-Report-Only. Always serve CSP via HTTP response headers from your edge or reverse proxy (Nginx, Caddy, Vercel headers, Cloudflare Workers).
4. Silent Telemetry Drops in connect-src
A restrictive connect-src 'self' policy stops data exfiltration, but frequently silences telemetry:
-
Error Trackers: Sentry, Datadog, or PostHog use dedicated ingestion subdomains (
https://*.ingest.sentry.io). -
WebSockets: Real-time channels (
wss://) require explicit inclusion inconnect-src. -
Beacon API: Browser telemetry sent via
navigator.sendBeacon()fails silently without throwing catchable JavaScript exceptions when blocked by CSP.
Before enforcement, map every background network endpoint including analytics beacons and token refresh services.
5. Client-Side CSS Injection and style-src
CSS-in-JS libraries and dynamic theme engines inject <style> tags directly into document.head at runtime.
Enforcing SHA-256 hashes on styles is fragile because runtime theme changes invalidate the hash. If you omit 'unsafe-inline' without configuring style nonces, components render unstyled. If you must allow 'unsafe-inline' for styles, pair it with tight connect-src and img-src boundaries to guard against CSS-based data exfiltration.
Auditing and Exporting Policies
To deploy CSP safely without breaking production:
-
Deploy in Report-Only First: Use
Content-Security-Policy-Report-Onlyfor 7–14 days to capture unexpected third-party scripts. -
Use a Policy Builder: When crafting directives across environments, tools like the Nutilz CSP Generator let you visually configure directives, spot conflicting overrides (like
'strict-dynamic'vs allowlists), and export ready-to-use configs for Nginx, Apache, Vercel, and Netlify. - Switch to Enforcement: Once incoming violation reports are clear, transition to standard enforcing headers.
Production Baseline Template
For modern SPAs and SSR frameworks:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM_BASE64' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com https://*.ingest.sentry.io;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
By accounting for 'strict-dynamic' overrides and bundler injection behaviors, you can lock down your web application without breaking core features. When testing or auditing directive configurations, you can use the Nutilz CSP Generator to preview and validate production-ready headers.
Top comments (0)