Content Security Policy (CSP): Architecting Strict CSP with Nonces and Hashes
Learn how to architect a secure, strict Content Security Policy (CSP) that prevents XSS without breaking your edge caches or dynamic inline scripts.
A strict Content Security Policy (CSP) is one of the most effective defenses against Cross-Site Scripting (XSS) vulnerabilities. By explicitly declaring which dynamic resources are allowed to load and execute, a CSP prevents attackers from injecting and running malicious payloads on your domain.
However, implementing a strict CSP in a modern web architecture—especially one relying on Server-Side Rendering (SSR), edge caching, or dynamic third-party tracking scripts—often results in catastrophic browser console errors and broken application logic.
Here is a comprehensive technical guide to understanding how CSP processing works, the difference between hash-based and nonce-based execution, and how to architect a strict policy that does not break your CDN caching strategy.
The Problem with unsafe-inline
A CSP defines which sources the browser is allowed to trust for scripts, styles, images, fonts, and other resources.
For example: script-src 'self' https://trusted-analytics.com;
However, most modern web applications rely on inline scripts (including <script type="module"> module scripts) injected directly into the HTML document:
<script>
window.__INITIAL_DATA__ = { user: "Admin" };
</script>
By default, a CSP will block all inline scripts. To bypass this, developers often resort to adding 'unsafe-inline' to their script-src directive. Do not do this. Adding 'unsafe-inline' completely bypasses the XSS protection of the CSP, rendering it effectively useless against the most common injection vectors.
To securely allow specific inline scripts to execute without using 'unsafe-inline', you must use either a Cryptographic Hash or a Cryptographic Nonce.
Hash-Based CSP
A hash-based CSP tells the browser to execute an inline script only if the SHA-256 hash of the script’s exact contents matches a hash explicitly declared in the CSP header.
If your HTML looks like this:
<script>console.log('Hello');</script>
You calculate the base64-encoded SHA-256 hash of the string console.log('Hello');, and include it in your HTTP response header:
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='
The Edge Caching Limitation
Hashes are highly secure but extremely brittle. If a single character, whitespace, or dynamically rendered variable inside the <script> tag changes, the hash changes, and the browser will block the script execution.
Because hashes must be statically known, they are impractical for dynamically generated inline scripts.
Nonce-Based CSP
A nonce (Number Used Once) is a randomly generated, unguessable cryptographic string generated by the server on every single page request.
The server injects this nonce into the CSP header, and then attaches it as a nonce attribute to every trusted <script> tag in the HTML document.
HTTP Header:
Content-Security-Policy: script-src 'nonce-rAnd0m123456'
HTML Document:
<script nonce="rAnd0m123456">
window.__INITIAL_DATA__ = { user: "Admin", cartTotal: 42 };
</script>
When the browser parses the HTML, it verifies that the nonce attribute on the script tag exactly matches the nonce in the HTTP header. If an attacker injects a malicious <script> tag, it will be blocked because the attacker cannot guess the randomly generated nonce for that specific page load.
The CDN Cache-Busting Problem
Because a nonce must be uniquely generated for every single HTTP request, you generally cannot safely serve the same nonce-bearing HTML to multiple requests from a shared cache.
If Cloudflare caches a page with nonce-ABC, and serves it to a different user an hour later, the header and the HTML will still match nonce-ABC. However, because the nonce is no longer a “Number Used Once,” an attacker who observes the cached nonce could theoretically construct an exploit. Furthermore, if you use edge middleware to generate the CSP header but serve the HTML body from a static cache, the header nonce will mismatch the body nonce, breaking your entire site.
Understanding strict-dynamic
In CSP Level 3, the 'strict-dynamic' keyword changes how script trust is established.
Instead of maintaining long allowlists of hostnames, a nonce-authorized root script may dynamically load additional scripts, and those scripts inherit trust automatically.
This significantly reduces maintenance overhead for modern JavaScript applications that rely on dynamic imports, module loaders, or third-party script injection.
However, 'strict-dynamic' is not a replacement for nonces. It extends the trust model after an initial nonce- or hash-authorized script has executed.
Trusted Types: Protecting the DOM
Trusted Types complement CSP by preventing DOM XSS sinks such as innerHTML, outerHTML, and insertAdjacentHTML from accepting arbitrary strings.
Instead, applications must explicitly create trusted HTML objects through a policy, making DOM injection significantly harder. This connects CSP’s network-level protection directly to modern browser DOM security.
Rolling Out CSP Safely
Begin with Content-Security-Policy-Report-Only.
The browser will report violations to a specified endpoint without actually blocking any resources. Once reports stabilize and you are confident your hashes or nonces cover all required scripts, you can safely migrate to an enforcing Content-Security-Policy header.
Which Approach Should You Choose?
When architecting your CSP, you must balance security with your infrastructure’s caching requirements:
| Architecture | Strategy | Recommendation |
|---|---|---|
| Static Site (SSG) | Edge-cached HTML | Use Hashes. Build processes can calculate hashes for static inline scripts and inject them into headers. |
| Dynamic SSR App | Uncached HTML | Use Nonces. Generate a unique nonce per request in middleware and pass it to the rendering engine. |
| Hybrid (ISR / Edge Cache) | Cached HTML + Middleware | Use Strict-Dynamic. It allows a single nonce on a root script to transitively trust dynamically imported scripts, simplifying cache invalidation. |
Architecture Summary
A production-grade, dynamic CSP architecture relies on edge middleware to generate nonces and inject them into both the request headers and the rendering engine pipeline:
flowchart TD
A[Browser Request] --> B[Edge Middleware]
B -->|Generates random Nonce| C[Set CSP HTTP Header]
B -->|Passes Nonce| D[SSR Rendering Engine]
D -->|Injects nonce="xxx"| E[Render HTML]
C --> F[Browser Response]
E --> F
Glossary
- CSP (Content Security Policy): An HTTP response header that declares approved sources of content that the browser may load.
- XSS (Cross-Site Scripting): A vulnerability where an attacker injects malicious client-side scripts into web pages viewed by other users.
'unsafe-inline': An insecure CSP directive that allows arbitrary inline scripts to execute. It disables XSS protection.'nonce-*'(Number Used Once): A cryptographic string randomly generated per request to authorize execution of inline scripts.'strict-dynamic': A CSP Level 3 directive that allows scripts authorized by a nonce to dynamically inject and execute other scripts, heavily simplifying third-party tag management.
Specification Status
| Technology | Status |
|---|---|
| CSP Level 2 (Hashes/Nonces) | Web Standard |
| CSP Level 3 (Strict-Dynamic) | Web Standard |
| Trusted Types | Emerging Standard |
| Reporting API | Web Standard |
Revision History
2026-07
- Initial documentation of CSP architecture.
- Added edge caching trade-offs for Hash vs Nonce strategies.