DEV Community

Rasika Dangamuwa
Rasika Dangamuwa

Posted on

Why Your Content Security Policy Fails in Production: 5 Single-Page App Edge Cases

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:;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';">
Enter fullscreen mode Exit fullscreen mode

While <meta> tags handle basic source controls, W3C specifications explicitly disallow key directives inside <meta> tags:

  • frame-ancestors (essential for preventing clickjacking)
  • report-to and report-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 in connect-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:

  1. Deploy in Report-Only First: Use Content-Security-Policy-Report-Only for 7–14 days to capture unexpected third-party scripts.
  2. 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.
  3. 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';
Enter fullscreen mode Exit fullscreen mode

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)