How We Hardened Our Content Security Policy with Per-Request Nonces
How We Hardened Our Content Security Policy with Per-Request Nonces
Release: v1.0.409 · Control: SEC-11 · File affected: next.config.ts
Background
Content Security Policy (CSP) is a browser-enforced security layer that restricts which scripts, styles, and other resources a page is permitted to load. It is one of the most effective mitigations against cross-site scripting (XSS) attacks.
Our platform uses Next.js and, like many Next.js applications, previously relied on 'unsafe-inline' in script-src to allow the framework's client-side hydration scripts to run. This was noted in the config as a known trade-off — but it also meant that any injected inline script would execute without restriction, making the CSP script-src directive functionally ineffective against XSS.
The Problem with 'unsafe-inline'
When 'unsafe-inline' is present in script-src, the browser will execute any <script> tag it encounters in the page, including those injected by an attacker through a reflected or stored XSS vector. The CSP header is still present, but its most important protection is switched off.
# Before — CSP with unsafe-inline (XSS protection nullified)
Content-Security-Policy:
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
This was flagged as SEC-11 in our internal security audit.
The Solution: Per-Request Nonces
Next.js 14+ supports a nonce-based CSP pattern natively. Instead of allowing all inline scripts, the server generates a unique, cryptographically random nonce on every HTTP request. That nonce is:
- Embedded in the
Content-Security-Policyresponse header. - Passed to the Next.js runtime so it can stamp the nonce onto its own hydration
<script>tags.
The browser only executes inline scripts whose nonce attribute matches the value in the CSP header for that specific response. Because the nonce changes with every request, an attacker cannot predict or reuse it.
# After — CSP with per-request nonce
Content-Security-Policy:
script-src 'self' 'nonce-rAnd0mP3rR3qu3stV4lu3';
style-src 'self' 'unsafe-inline';
<!-- Next.js hydration script now carries the matching nonce -->
<script nonce="rAnd0mP3rR3qu3stV4lu3">/* hydration */</script>
What Was Changed
next.config.ts
- Removed
'unsafe-inline'fromscript-src. - Added
'nonce-{value}'placeholder, resolved at request time by middleware.
Next.js Middleware
- On every incoming request, the middleware generates a fresh nonce using a secure random source.
- The nonce is set in the
Content-Security-Policyheader on the response. - The nonce is forwarded to Next.js so the framework can attach it to internal
<script>tags during server-side rendering.
What Remains to Be Done
style-src still carries 'unsafe-inline'. While this is a lower-risk surface (CSS-based attacks are generally harder to exploit than script injection), we plan to replace it with 'unsafe-hashes' scoped to the SHA-256 hashes of the specific inline styles we actually use. This will be tracked as a follow-up hardening task.
Impact for Self-Hosted Deployments
If you run a self-hosted instance of this platform and have added custom inline <script> tags:
- Those scripts must receive the server-generated nonce as a
nonceattribute to continue working. - Scripts loaded via
srcattributes from allowed origins are unaffected. - Scripts injected at runtime by third-party tools without a nonce will be blocked — review any tag manager or analytics snippets you have added.