skip to content

A service answers `Access-Control-Allow-Origin: null` so a sandboxed preview can call it. Who has just been granted read access?

level: seniorimportance: nice to knowfreq 30%

answer

  1. the token names a class
  2. not one preview frame
  3. missing header is not the value
  4. reflection produces it accidentally
  5. cheap for anyone to present

basics

~20 s

Every context whose origin serializes to that value, all at once. The token names no particular party, so it cannot identify the preview it was added for, and a document can be placed in such a context deliberately.

solid answer

~50 s

The grammar of the grant permits either a serialized origin or the literal token `null`, and only the first names anybody. `null` is what several unrelated kinds of context serialize to, so allowing it admits that whole class rather than the one preview frame the entry was added for — and an attacker can arrange for their own document to run in such a context, which makes the value cheap to present. The confusion that keeps it alive is that a *missing* `Access-Control-Allow-Origin` header and the *value* `null` look like the same thing and are not: the missing header is a refusal, the token is a grant. If the preview genuinely needs access, give it a real origin to run in rather than granting the class; if it cannot have one, the endpoint should not be reachable from it.

code

http · 8 lines
http
GET /v1/sessions HTTP/1.1
Host: api.conf-schedule.example
Origin: null

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

go deeper

for a junior

Note that this header's value can be a real origin or the literal token, and that only the real origin identifies who is being allowed.

for a middle

Explain the difference between an absent grant header and a grant whose value is the token, and why the two produce opposite outcomes in the browser.

for a senior

Show how the entry gets added from a failure message, why the class it admits is larger than the case that motivated it, and how reflection produces it with no configuration change.

for a principal

Generalise the rule: every entry in a grant list should name a party with an owner. An entry naming a state instead is a defect in the list's design, not a tuning value.

## A grant addressed to nobody The value of `Access-Control-Allow-Origin` is defined to be either a serialized origin — `scheme://host[:port]` — or the wildcard, or the literal token `null`, spelled in lower case and compared case-sensitively. The first of those identifies one party. The last identifies none. A browser stamps `Origin: null` on requests from contexts that have no ordinary scheme/host/port identity to report. Which contexts those are is the browser platform's own subject; what matters at the grant end is the arithmetic: **many unrelated contexts produce the same value, so a grant naming that value admits all of them together.** There is no version of this where the entry means "our preview frame", because the value does not carry that information and never did. ## How the entry gets added It is nearly always a diagnosis run backwards. A preview or embedded view stops working; the failure message names the value `null`; somebody adds `null` to the allowed set, and it works. The change was tested against exactly one context and shipped as though it applied to exactly one context. The same thing happens through reflection: a service that echoes whatever arrives into the grant header will echo `null` too, which produces this state without anyone deciding on it. ## Why the value is cheap to present The reason this belongs in an interview rather than a footnote is the asymmetry: - The token is **not a secret and not a capability** — presenting it costs nothing, and an attacker can host a document in a context that serializes this way. - It is **not scoped to your service or your preview** — the same value comes from anywhere. - It is often paired with `Access-Control-Allow-Credentials: true`, because the entry was added to make an authenticated view work, so the reads it permits are of a logged-in user's data. ## null is not "no grant" These three are distinct states and are routinely collapsed into one: | What is on the response | What it means | |---|---| | No `Access-Control-Allow-Origin` header at all | No grant was issued; the browser withholds the response from script | | `Access-Control-Allow-Origin: null` | A grant was issued, to the whole class of contexts that serialize this way | | A JSON `null` somewhere in the body | Nothing to do with the grant; the body is not consulted by the check | The specification is deliberate about the first two reading differently, and an answer that treats the header value as a polite way of saying "none" has inverted the mechanism. ## What to do instead 1. **Give the caller a real origin.** If the view needs to read your data, serve it from a host and scheme you control, and allow that origin exactly. 2. **If it cannot have one, do not grant.** A context with no identity cannot be authorised; treat the requirement as a signal that the data should reach it another way, for example rendered by something that already has the identity. 3. **Remove the entry when the case that added it disappears.** These survive for years because nothing fails when they are wrong. 4. **Check for it deliberately**, by sending the value and reading what comes back, rather than by reviewing configuration — a service that reflects will produce this grant without any configuration mentioning it. The broader habit is the useful part. Every entry in a grant list should name a party you can point at. An entry that names a *state* rather than a party is not an allow-list entry at all.

  • How does this state arise in a service whose configuration never mentions the value?
    Through reflection. A service that copies the arriving `Origin` into `Access-Control-Allow-Origin` copies this value like any other, so the grant appears at runtime with nothing in the configuration to review. That is why detecting it means sending the value and reading the answer, rather than reading config.
  • Is omitting `Access-Control-Allow-Origin` the same as answering with the value `null`?
    No, and treating them as equivalent inverts the mechanism. An absent header means no grant was issued, and a conforming browser withholds the response from the calling script. The literal token is a grant whose named party happens to be a whole class of contexts, so the browser releases the response to any of them.

It is an invitation addressed to Occupant. It admits whoever is holding it, and no amount of knowing who you meant to send it to changes that.

saying these in an interview costs you the question

  • The value null means no origin was supplied, so it grants nothing.
  • Only our own sandboxed preview can present a null origin.
  • A missing Access-Control-Allow-Origin header is the same as the value null.
  • An attacker cannot arrange to present a null origin deliberately.
  • Granting null is harmless precisely because it names nobody.