Why does a Content-Security-Policy hash-source stop matching an inline script block whose text is generated per response?
answer
- the digest covers bytes, not meaning
- whitespace and indentation count
- UTF-8 encoding of the element's text
- per-response text, per-response digest
- a nonce ignores content entirely
basics
~20 sA 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.
solid answer
~40 sA `hash-source` is a `hash-algorithm` of `sha256`, `sha384` or `sha512`, a hyphen, and a `base64-value`. To decide whether an inline block may run, a conforming browser digests the UTF-8 encoding of that element's text exactly as it was parsed, encodes the digest, and compares it with each hash in the list. Every byte counts: indentation, a trailing newline, a changed quote style, and above all any value interpolated into the block. So a block carrying a per-response value - a reference number, a timestamp, a rendered identifier - has different text on every response and can never be pinned by a fixed digest. That is exactly the case a `nonce-source` covers, because a nonce is matched against an attribute and never looks at the content at all.
code
html · 7 lines<script>
initStepTwo();
</script>
<script>
initStepTwo({ reference: "BEN-2026-4471" });
</script>go deeper
Remember that a hash names exact content while a nonce names an element. If the text inside the block can change between responses, a fixed digest cannot describe it.
Explain the input to the digest - the UTF-8 encoding of the element's text as parsed - and why indentation, a trailing newline or an interpolated value all move it, so per-response content needs a nonce.
Show the diagnosis: a block refused on one environment and allowed on another is a byte difference no reviewer sees, and the fix is to decide whether that block should ever have been pinned by content.
Frame it as a coupling question: a hash binds the policy to the exact output of a rendering and formatting pipeline, so every tool that can touch that text becomes part of the release gate.
## What is actually hashed A `hash-source` looks like `'sha256-BASE64VALUE'`: a `hash-algorithm` token, a hyphen, and a `base64-value`. The three algorithm tokens a source list may use are `sha256`, `sha384` and `sha512`; nothing else is a hash-source. For an inline block the input to the digest is **the UTF-8 encoding of the element's text, exactly as parsed**. That means: - Leading and trailing whitespace inside the element is part of the input. Re-indenting the block changes the digest. - A newline added or removed at either end changes the digest. - Reformatting, minifying or reordering the statements changes the digest even though the behaviour is identical. - A comment added inside the block changes the digest. - Any value templated into the block changes the digest on every response that renders a different value. No trimming, no normalisation, no parsing of the content as code happens first. The digest is taken over bytes, and the bytes are whatever the element's text turned out to be. ## Why generated content is the case that breaks A hash is a statement about *content*: "this exact text may run". It can only be written down in advance if the text is fixed in advance. An inline block that interpolates anything - an applicant's reference, a step number, a rendered date - produces different text per response, so any digest written into the policy is stale the moment it is written. The symptom is unhelpfully specific: the block runs for one request, or on one environment, and is refused everywhere else. Two blocks that look the same in a reviewer's editor differ by a byte that never appears on screen. | | `nonce-source` | `hash-source` | |---|---|---| | Matched against | the element's `nonce` attribute | a digest of the element's text | | Sensitive to content | no | totally | | Value fixed across responses | no | yes | | Survives reformatting | yes | no | | Needs markup changed | yes, an attribute | no | The last two rows are the real decision. A hash asks nothing of the markup, which is why it suits a block that never changes - a small fixed bootstrap, a feature-detection stub. A nonce asks for an attribute but says nothing about the content, which is why it is the only one of the two that can cover text generated per response. ## One asymmetry worth knowing The two expressions are compared differently. A nonce is a **literal string match**: the characters in the policy and the characters in the attribute either agree or they do not. A hash comparison is more forgiving in exactly one respect: the standard base64 alphabet and the base64url alphabet are treated as equivalent, so a digest written with `-` and `_` matches one written with `+` and `/`. That is a concession about how the same bytes were spelled, and it exists for hashes only - it is not a general "values get normalised" rule. ## The inline hash and the external hash are not the same number CSP Level 3 also lets a `hash-source` authorise an **external** script: if the element carries integrity metadata and every item of that metadata is listed in the source list as a hash-source, the fetch matches. The trap is that the two digests are taken over **different inputs**: 1. The inline hash is taken over the UTF-8 encoding of the element's text - what the parser produced, after entity references were resolved and with the surrounding template indentation included. 2. The integrity metadata for an external file is computed over the raw bytes of the fetched resource. Those inputs coincide only if they are byte-for-byte identical, and in practice template indentation, a trailing newline, or newline normalisation makes them differ. So moving a block from inline to a file, or the reverse, generally requires recomputing the value rather than reusing it. Treat "the hash of that script" as ambiguous until someone says which input it was taken over. ## How to choose between the two - Fixed, small, never-templated inline content, and markup you would rather not touch: a hash is a clean fit. - Content that varies per response, or a large number of blocks: a nonce, because one value in the policy covers every element carrying the attribute. - A block that is stable today but sits in a file a formatter will reach: a hash is a maintenance liability, and the failure arrives as a blocked script rather than a failed build. - Several fixed blocks: several hash-sources may appear in one source list, and any one of them matching is enough for the element it describes. And remember that either choice makes `'unsafe-inline'` be ignored in the same list, so the switch is never additive: whatever the new expression does not name stops running.
- Does the base64 spelling of a hash-source have to match the browser's own spelling exactly?No. For a hash-source the standard base64 alphabet and the base64url alphabet are treated as equivalent, so `-` and `_` match `+` and `/`. That tolerance is specific to hashes; a nonce is compared as a literal string with no equivalence at all.
- Which hash-algorithm tokens may open a hash-source?`sha256`, `sha384` and `sha512`. Anything else is not a hash-source, so the token is simply not recognised as one and cannot authorise content - and, not being a hash-source, it also does not suppress `'unsafe-inline'` in that list.
- Can a hash-source ever authorise an external script file?In CSP Level 3, yes: if the element carries integrity metadata and every item of it is listed in the source list as a hash-source, the fetch matches. The value is not interchangeable with the inline hash of the same code, because the two are computed over different inputs.
A nonce is a wristband handed out at the door: staff check the band, not who you are or what you are about to say. A hash is a guest list that quotes what you may say word for word - change one word of it and you are no longer on the list.
saying these in an interview costs you the question
- Thinks the browser trims whitespace before digesting the block
- Assumes the digest survives reformatting or minification
- Says a hash covers any block with equivalent behaviour
- Believes sha1 or md5 are usable as a hash-algorithm token
- Expects the inline hash of code to equal its integrity value as a file
- Thinks a hash-source can be recomputed by the browser per response