skip to content

An `Origin` header arrives as the literal `null` after a request followed a redirect across hosts — what produced that value?

level: seniorimportance: nice to knowfreq 32%

answer

  1. the chain remembers where it started
  2. one boundary crossing is enough
  3. identity discarded, not preserved
  4. four ASCII bytes, lower case only
  5. a value, not a missing field

basics

~20 s

Once a request's redirect chain crosses an origin boundary, the request's origin is treated as tainted and serializes to the literal four-byte value null instead of the caller's origin. The final host therefore receives no usable caller identity.

solid answer

~40 s

A request carries a taint that tracks whether its redirect chain has stayed within one origin. When a hop crosses that boundary, the request's origin becomes a redirect-tainted one, and the grammar `origin-or-null = serialized-origin / %s"null" ; case-sensitive` allows exactly one thing in that case: the literal lower-case ASCII string `null`. So an arrivals board whose departures feed is reached through a cross-origin redirect arrives at the final host with **no caller identity at all** — not the board's origin, and not an empty field either. The value is four bytes of data, which is a different situation from the field being absent, and the case sensitivity in the grammar means `NULL` is not the same token.

code

http · 6 lines
http
POST /v1/subscribe HTTP/1.1
Host: api.departures.example
Origin: https://board.example.org
Content-Type: application/json

{"stop":42}

go deeper

for a junior

Know that the value can be the literal word null, and that this is a value rather than a missing field. It means the request has no caller identity attached to it.

for a middle

Explain the mechanism: a chain that crosses an origin boundary taints the request's origin, and the grammar permits only the case-sensitive token null in place of a serialized origin.

for a senior

Show the diagnosis. Follow the whole chain rather than the last hop, identify the crossing that discarded the identity, and recognise that a direct reproduction against the final host cannot reproduce it.

for a principal

The structural point is that a redirect across hosts silently destroys caller attribution for everything downstream, so a feed topology that relies on cross-host redirects has given up an input that any decision on the far side was using.

The arrivals board calls one departures host. That host answers a subscription call with a redirect to a second host that serves the live feed. The first hop arrives carrying the board's origin; the hop after the redirect arrives carrying the literal string `null`, and nothing in the board's own source changed between them. ## Redirect taint A request does not simply forget where it came from when it is redirected. It carries a taint that records whether the chain it has followed has stayed inside one origin. When a hop takes the request to an origin that the chain has not been within, the request's origin is replaced by a tainted one, and the serialization that produces the header value has only one thing it is permitted to emit for it: ``` origin-or-null = serialized-origin / %s"null" ; case-sensitive ``` The `%s` prefix on that token is not decoration — it marks the string as case-sensitive, so the value is exactly `null` in lower case and `NULL` is a different, wrong token. ## What the final host actually receives ```http POST /v1/subscribe HTTP/1.1 Host: api.departures.example Origin: https://board.example.org ``` and, after the chain crosses into another host: ```http POST /v1/subscribe HTTP/1.1 Host: feeds.departures.example Origin: null ``` The field is still there. Its value is four ASCII bytes that identify nobody. This is the distinction that makes the failure confusing: - **A value of `null`** is a present field carrying a deliberate token, produced because the caller identity was discarded. - **No field at all** is a different situation with different causes — for instance a safe-method request that never needed one. - **A JSON `null` in a body** is a third thing entirely and shares nothing with either but the spelling. ## Why the identity is discarded rather than preserved The purpose of the value is to name the security context that initiated the request so that the receiving host can make a decision about it. Once a chain has been bounced through an origin the initiator did not choose, carrying the initiator's name forward would attribute a request to a context that no longer controls where it went. The specification's answer is to stop naming anyone: the request continues, and it continues anonymously. The practical consequences are worth stating plainly: 1. **No allow-list entry can match it as an origin.** `null` is not a serialized origin; it is the token used in place of one, and it is not the name of any host. 2. **The failure appears one hop after the change that caused it.** The redirect is often introduced by an infrastructure change — a feed moved to a second host, a path retired — and the board's code is untouched, so the board is the first place people look and the last place the cause is. 3. **It is not a browser defect and not a stripped field.** The value was produced deliberately by the serialization, and it will be produced identically every time that chain is followed. 4. **Reproducing the first hop alone hides it.** A request issued directly at the final host carries whatever its sender chose and never acquires the taint, so the reproduction succeeds while the real path fails. ## Diagnosing it Read the chain, not just the last request. The question to answer is whether any hop moved the request to a host outside the origin it started in; the first hop that does is the one that replaced the identity. The repair is nearly always structural rather than a configuration edit: have the caller address the host that will actually serve the response, so the chain never crosses a boundary and the request keeps its serialized origin the whole way. ## Not the only producer of that value Redirect taint is the reason the token appears in this scenario, and it is worth knowing that it is not the sole way a request can end up without a namable initiating context — a document can be in a state where its origin has no host to serialize either. What matters for reading a wire capture is the shape of the conclusion: **the token names no one**, so whatever decision the receiving host makes, it makes without knowing who called.

  • Is `Origin: null` the same as the request arriving with no `Origin` field?
    No. One is a present field whose value is four ASCII bytes, deliberately emitted in place of a caller identity; the other is a field that was never attached, which happens for its own reasons such as a safe-method request. Anything reading the wire has to distinguish a token from an absence, because the two situations have different causes.
  • Why does reproducing the call directly against the final host not show the problem?
    Because the taint is a property of the chain, not of the destination. A request issued straight at the final host has followed no cross-origin hop, so nothing replaces its origin and it arrives with an ordinary value. The failure only appears when the same path the browser took is followed hop by hop.
  • Can the token be matched by writing `null` into a list of allowed origins?
    Matching it as a string is mechanically possible, but it is worth being clear about what it would mean: the token names no host, so every request that acquired it — from any caller, through any chain — carries the identical value. It is the absence of an identity, not a narrow one.

saying these in an interview costs you the question

  • Reads an Origin of null as the field being absent
  • Assumes the caller's origin survives every redirect hop
  • Thinks a browser dropped the value because of a bug
  • Believes the value is a JSON null rather than four ASCII bytes
  • Expects an upper-case NULL to be the same token