Content Security Policy Evolution and CDN Edge Enforcement

· James Thornton · 11 min read

Content Security Policy is the browser's primary defense against cross-site scripting. A CSP header tells the browser exactly which sources of content — scripts, styles, images, fonts, connections — are legitimate for the page. Any content that does not match the policy is blocked, even if an attacker manages to inject it into the page's HTML. For CDN-proxied applications, CSP introduces unique challenges: the CDN's distributed caching layer must not cache CSP headers that contain per-request nonces, edge workers must inject nonces into HTML responses at serving time, and reporting must aggregate violations across hundreds of PoPs.

CSP Level Evolution

Content Security Policy has evolved through three major levels, each adding capabilities critical for CDN architectures.

CSP Level 1: Source Allowlists

The original CSP specification (2012) introduced directive-based source allowlists. Each directive (script-src, style-src, img-src, connect-src) specifies which origins can provide that content type. For CDN deployments, this meant whitelisting every CDN hostname that serves scripts, styles, and images — often multiple subdomains per CDN provider.

The problem with source allowlists is maintenance burden. CDN providers rotate hostnames, add new edge domains, and distribute content across multiple subdomains. A CSP that allowlists cdn.example.com breaks when the CDN starts serving from cdn2.example.com. Wildcard allowlists (*.cdn-provider.com) reduce breakage but widen the attack surface — any content on the CDN provider's infrastructure becomes trusted.

CSP Level 2: Nonces and Hashes

CSP Level 2 (2015) introduced two alternatives to source allowlists: nonces and hashes. A nonce is a random value generated per response and added to both the CSP header ('nonce-abc123') and each trusted script tag (<script nonce="abc123">). The browser only executes scripts with a matching nonce. A hash is the cryptographic hash of the inline script content ('sha256-...') — the browser only executes inline scripts whose content matches the hash.

Nonces are the preferred approach for CDN architectures because they accommodate dynamic content without recomputing hashes. The CDN edge worker generates a fresh nonce on each response, injects it into the CSP header and all trusted script tags, and the browser enforces the policy. Hash-based CSP works for static inline scripts but requires recalculation whenever the script content changes — impractical for pages with dynamic content.

CSP Level 3: strict-dynamic and Trusted Types

CSP Level 3 (2018+) introduced the directives that make CSP practical for modern CDN architectures. The 'strict-dynamic' directive is the most impactful: it tells the browser that any script loaded by a nonced (trusted) script should also be trusted. This eliminates the need to maintain source allowlists for dynamically loaded scripts — a critical improvement for CDN applications that load third-party scripts, analytics, and A/B testing libraries dynamically.

Level 3 also separates script-src into script-src-elem (controls <script> elements) and script-src-attr (controls inline event handlers like onclick). This separation allows CDN-proxied pages to allow script elements while blocking inline event handlers, providing tighter XSS protection without breaking script loading.

CSP Evolution Timeline for CDN Architectures 2012 Level 1 Source allowlists script-src, style-src 2015 Level 2 Nonces + hashes Per-request tokens 2018+ Level 3 strict-dynamic Trusted Types Level 1: Must whitelist every CDN hostname → maintenance nightmare script-src cdn1.example.com cdn2.example.com cdn3.example.com ... Level 3: Nonce + strict-dynamic → CDN hostnames irrelevant script-src 'strict-dynamic' 'nonce-{random}' → trust chain from initial script
Figure 1: CSP evolution from source allowlists to nonce-based trust chains — strict-dynamic eliminates CDN hostname maintenance.

Nonce Generation at CDN Edge

Implementing nonce-based CSP at CDN edge requires generating a fresh nonce on every response and injecting it into both the CSP header and the HTML body. This creates a tension with CDN caching: a response with a per-request nonce cannot be cached (the nonce would be reused, breaking CSP for subsequent requests).

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages

Post-Cache Nonce Injection

The solution is post-cache nonce injection. The origin server generates HTML with a placeholder token in script nonce attributes (e.g., nonce="CSP_NONCE_PLACEHOLDER"). The CDN caches this HTML with the placeholder. On each response, the edge worker generates a fresh nonce, replaces all placeholder tokens in the cached HTML with the real nonce, and adds the CSP header with the matching nonce.

This approach preserves full CDN caching while providing per-request nonces. The HTML transformation adds 1–3ms of edge processing latency (depending on page size and the number of replacements), which is negligible compared to the round-trip to origin that would be required without caching.

Edge Worker Implementation

The edge worker implements nonce injection using streaming HTML transformation. Rather than buffering the entire response, replacing tokens, and then sending the result, the worker uses the CDN platform's HTMLRewriter API (or equivalent) to transform the response as it streams through the edge. This keeps memory usage constant regardless of page size and minimizes time-to-first-byte.

The nonce generation uses crypto.getRandomValues() to produce 128 bits of randomness, encoded as base64. The nonce must be at least 128 bits to prevent brute-force guessing — an attacker who can predict the nonce can bypass CSP entirely. The edge runtime's CSPRNG (cryptographically secure pseudorandom number generator) is the only acceptable source; never use Math.random() or time-based values.

strict-dynamic for CDN Script Chains

Modern web applications loaded through CDNs typically have complex script loading chains. The initial HTML loads a bootstrap script from the CDN, which loads the application framework, which loads route-specific code, which loads third-party analytics and feature flags. Without 'strict-dynamic', each script in the chain would need its own nonce — requiring the edge worker to inject nonces into dynamically generated script tags within the bootstrap script, which is not feasible.

'strict-dynamic' solves this by establishing a trust chain. Only the initial script tags in the HTML need nonces. Any script loaded by a nonced script (via document.createElement('script') or dynamic import()) inherits trust automatically. The CDN edge worker only needs to nonce the scripts in the initial HTML response — the rest of the loading chain is covered.

When 'strict-dynamic' is present, source allowlists are ignored in browsers that support Level 3. This means the policy script-src 'strict-dynamic' 'nonce-abc' https://cdn.example.com behaves as script-src 'strict-dynamic' 'nonce-abc' in modern browsers (the host allowlist is ignored) and as script-src https://cdn.example.com in older browsers (the nonce and strict-dynamic are ignored). This provides graceful degradation.

CSP Reporting Architecture

CSP violation reporting is essential for monitoring policy effectiveness and detecting XSS attacks. When a browser blocks content that violates the CSP, it can send a violation report to a reporting endpoint specified in the policy.

Report-To vs report-uri

The legacy report-uri directive sends violation reports as POST requests to a URL. The modern report-to directive uses the Reporting API, which batches reports, supports multiple endpoints, and provides structured error information. CDN architectures should use both for maximum browser coverage: report-to csp-endpoint; report-uri /csp-report.

The reporting endpoint must handle high volumes. A single CSP misconfiguration on a popular page can generate millions of violation reports in minutes. The CDN edge worker can mitigate this by sampling reports at the edge (forwarding only 1–10% of violations to the reporting endpoint) and aggregating violations by directive and blocked URL before forwarding.

Report-Only Mode for Policy Testing

The Content-Security-Policy-Report-Only header enforces the policy in report-only mode: violations are reported but not blocked. This is critical for testing new CSP policies on CDN-proxied sites — a misconfigured CSP can break an entire site instantly, and CDN-wide deployment means the breakage affects all users simultaneously.

The CDN edge worker can implement a gradual CSP rollout: deploy the new policy in report-only mode to 100% of traffic, monitor violation reports for false positives (legitimate content blocked by the new policy), adjust the policy to eliminate false positives, then switch from report-only to enforcing mode. This can be done at the edge worker level without origin changes, using feature flags or gradual percentage-based rollout.

CSP Reporting Architecture at CDN Edge CSP Violation Browser detects blocked content Edge Sampler 1-10% sampling Dedup + aggregate Report Collector Structured storage Trend analysis Security Team Dashboard alerts Policy tuning Rollout workflow: Report-Only mode → Monitor false positives → Fix policy → Enforce Edge worker manages rollout via feature flags — no origin redeployment needed
Figure 2: CSP violation reporting architecture at CDN edge — edge sampling prevents report floods while preserving security visibility.

Trusted Types at CDN Edge

Trusted Types is a relatively recent addition to the browser security model that complements CSP. While CSP controls which scripts can execute, Trusted Types controls what data can reach DOM injection sinks (innerHTML, document.write, eval, setTimeout with strings). Even if an attacker bypasses CSP, Trusted Types provides a second layer of defense by preventing unsafe DOM manipulation.

The CDN edge worker enables Trusted Types by adding require-trusted-types-for 'script' to the CSP header. This tells the browser to reject any assignment to a DOM injection sink unless the value is a Trusted Type created by a registered policy. The edge can also specify which policies are allowed with trusted-types policy1 policy2, limiting the application to a known set of sanitization policies.

For CDN-served single-page applications, Trusted Types provide significant protection against cache poisoning attacks. If an attacker manages to inject malicious content into a cached response, Trusted Types prevent the injected content from being executed through DOM manipulation, even if the content passes CSP checks (e.g., because it uses a trusted inline script pattern).

CSP for Multi-CDN Architectures

Applications served through multiple CDN providers face CSP challenges because each provider uses different hostnames. A strict CSP that allowlists one CDN's hostnames breaks when traffic is routed through a different CDN during failover or load balancing.

The 'strict-dynamic' approach solves this for script loading — nonce-based trust is hostname-independent. For other resource types (images, styles, fonts) that still use source allowlists, the edge worker at each CDN must add its own CDN's hostnames to the CSP in addition to the base policy. This requires CDN-specific edge worker configuration, which adds operational complexity but maintains security.

An alternative is to use self as the primary source in CSP directives combined with subresource integrity (SRI) hashes for critical external resources. The self keyword always refers to the serving domain, which is the CDN domain when the CDN proxies the application. Combined with SRI verification at the edge, this provides origin-independent CSP that works across any CDN provider.

Measuring CSP Effectiveness

CSP effectiveness is measured through violation report analysis, coverage metrics, and attack detection rates. The CDN's monitoring infrastructure should track the number of CSP violations per page, the top violated directives (indicating policy gaps or attack attempts), the ratio of report-only violations to enforced violations (indicating policy maturity), and the unique blocked URLs (indicating the diversity of blocked content sources).

A well-tuned CSP for a CDN-proxied application should have zero report-only violations (the policy is tight enough for enforcement), a steady low rate of enforced violations (indicating ongoing XSS attempts being blocked), and no violations from legitimate first-party or approved third-party scripts. A sudden spike in violations may indicate a new XSS vector, a compromised third-party script, or a cache poisoning attempt — each requiring immediate investigation by the security team.

External References

References & Standards: Cloudflare documentation · W3C Web Performance