skip to content

questions

4

A Content-Security-Policy sends script-src 'nonce-abc123'; what must an inline script element carry, and how is that value matched?

level: juniorimportance: must knowfreq 62%

answer

  1. an attribute, not a hostname
  2. the policy names one element
  3. prefix and quotes are not the value
  4. compared as a literal string
  5. ASCII case-sensitive, never decoded

basics

~10 s

The element needs a nonce content attribute holding exactly the base64-value from the policy: 'nonce-abc123' matches nonce="abc123". The comparison is a literal ASCII case-sensitive string match, with no base64 decoding and no normalisation.

solid answer

~40 s

A `nonce-source` is a source expression written `'nonce-` plus a `base64-value` plus a closing quote. The element half is the HTML `nonce` content attribute, and its value is the `base64-value` alone - the `'nonce-` prefix and the quotes belong to the policy grammar and never appear in the markup. When a conforming browser is about to run a script element, it walks the applicable source list, takes the `base64-value` of every `nonce-source` it finds, and compares each against the element's nonce as a literal ASCII case-sensitive string. Nothing is decoded, folded or trimmed, so one differing byte blocks the element. A matching nonce authorises that one element, inline or external; every other inline block on the page stays blocked.

code

http · 3 lines
http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'; base-uri 'none'

go deeper

for a junior

Recall the pair: a 'nonce-VALUE' expression in the policy and nonce="VALUE" on the element, with the prefix and quotes left out of the markup. Know that one mismatched byte blocks the element.

for a middle

Explain the matching itself - a literal ASCII case-sensitive comparison with no decoding - and that a nonce authorises the element, so a nonced external script loads regardless of the host sources listed beside it.

for a senior

Show that you debug this from the two strings rather than by eye: print the policy that actually shipped and the attribute that actually rendered, and name which half of the pair went missing.

for a principal

Frame the trade-off of naming elements instead of origins: an element-scoped grant shrinks the allowlist to nothing, but it makes every surface that emits markup part of the security boundary.

## The two halves of the mechanism A nonce authorisation always has two halves, and both have to be on the same response. - The **policy half** is a `nonce-source`: one `source-expression` inside the `serialized-source-list` of a fetch directive such as `script-src` or `style-src`. It is spelled `'nonce-` followed by a `base64-value` followed by a closing single quote - for example `'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'`. - The **document half** is the `nonce` content attribute on the element: `<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">`. The attribute value is the `base64-value` **only**. The `'nonce-` prefix and the surrounding quotes are policy grammar, not markup, and copying them into the attribute is the classic first-time failure: `nonce="'nonce-EDNnf...'"` never matches anything. ## How a conforming browser matches them 1. The response header is parsed into a policy, and each directive's value into a source list of `source-expression` tokens separated by whitespace. 2. When the document is about to execute a script element, the browser consults the source list that applies to that element. 3. For every token in that list that matches the `nonce-source` grammar, it extracts the `base64-value` part. 4. It compares that string with the element's nonce, ASCII case-sensitive, byte for byte. 5. A single match authorises the element. No match, and the element is blocked unless some other expression in the same list allows it. What the comparison deliberately is **not**: - It is not a base64 decode. The grammar restricts which characters may appear, but the bytes behind them are never recovered or compared. - It is not case-insensitive. `abc123` and `ABC123` are two different nonces. - It is not a prefix or substring match, and padding `=` characters are part of the string like any other character. - It does not trim the attribute value, so a stray space or newline inside the quotes is a mismatch. ## What a nonce authorises, next to the other source expressions | Source expression | What it authorises | Inline blocks | External script | |---|---|---|---| | `'self'` | same-origin URLs | none | yes, same origin | | `'unsafe-inline'` | every inline block of that type | all of them | no | | `'nonce-...'` | the elements carrying that exact value | only the named ones | yes, the named ones | | `'sha256-...'` | content whose digest equals the value | by content | only with integrity metadata | The row that surprises people is the nonce row's last column: a `nonce` attribute on an external `<script src="...">` authorises that fetch as well, whatever host sources the directive does or does not contain. The nonce names an *element*, not a kind of content. The same attribute is defined on `<style>` elements and on the `<link>` element that loads a stylesheet, where it is matched against `style-src` in exactly the same way. A nonce listed only in `script-src` does nothing for a `<style>` element. ## The value is per element, and per response The attribute is not inherited and not ambient. Two inline blocks that both need to run both need the attribute, and the same value in the policy covers both - a source list may also carry several different `nonce-source` tokens at once, and any one of them matching is enough. Because the policy travels on the response and the attributes travel in the same response body, the pair is only ever meaningful together. A page whose markup carries an attribute while the policy on that response has no `nonce-source` at all gets no benefit from it: there is nothing to match, and the block runs only if something else in the source list permits it. ## Where this goes wrong in practice - The attribute and the policy differ by one byte or by case - the single most common cause of "the policy blocked my own script". - The `'nonce-` prefix or the quotes were pasted into the attribute. - The block was moved into a shared markup fragment that renders without the attribute, so it silently stops running. - The element is a `<style>` block but the nonce is only listed under `script-src`. - The element carries a nonce and the policy carries one, but they were produced by two different parts of the response and no longer agree. Each of these is diagnosable from the policy string and the markup alone: print both, and compare the two strings character by character rather than by eye.

  • Is the nonce compared after base64-decoding it?
    No. The `base64-value` grammar restricts which characters may appear, but the match is a literal ASCII case-sensitive comparison of the two strings. Two spellings that would decode to the same bytes are still two different nonces, and one of them will be blocked.
  • The policy carries a nonce-source and an inline script element carries no nonce attribute at all - what happens?
    It is blocked, unless the same source list independently allows it - for instance a `hash-source` matching that block's text. It cannot fall back to `'unsafe-inline'`, because the presence of the `nonce-source` makes that keyword be ignored.
  • Does a nonce on an external script element have to agree with the host sources in the same directive?
    No. A matching nonce authorises that element on its own, so a nonced `<script src="...">` loads even from a host the directive never lists. That is why a nonce is a statement about an element rather than about an origin.

saying these in an interview costs you the question

  • Thinks the value is base64-decoded and compared as bytes
  • Copies the 'nonce- prefix and quotes into the HTML attribute
  • Says a nonce only ever authorises inline blocks, never a nonced external script
  • Assumes one nonce attribute covers every element in the document
  • Expects a trimmed or case-insensitive comparison of the attribute
  • Confuses this nonce with a cryptographic nonce in an encryption scheme
open as a page

Why does adding a nonce-source to a Content-Security-Policy script-src make an 'unsafe-inline' token in that same list stop working?

level: middleimportance: must knowfreq 55%

basics

~20 s

A source list containing any nonce-source or hash-source makes a conforming browser ignore 'unsafe-inline' for that type. The weaker keyword is left in deliberately, so clients too old to understand the newer tokens still run the page.

open as a page

Why does a Content-Security-Policy hash-source stop matching an inline script block whose text is generated per response?

level: middleimportance: should knowfreq 46%

basics

~20 s

A hash-source matches only when the digest of the UTF-8 encoding of the element's text equals the value written in the policy. Text that changes per response changes those bytes, so a precomputed digest never matches again.

open as a page

In a nonce-based Content-Security-Policy, which rules stop injected markup from capturing or reusing a legitimate nonce value?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Three: the value is moved into the element's [[CryptographicNonce]] slot and the nonce content attribute is blanked, so attribute readers see nothing; the 'Is element nonceable?' check refuses a nonce on markup that looks like a half-open tag; and base-uri closes nonce retargeting.

open as a page