Why does the departures host see an `Origin` header on a request the browser sent to that same host, and what does its presence prove?
answer
- presence is not direction
- two independent triggers, not one
- safe methods are the quiet ones
- not GET, not HEAD, so stamped
- the value answers, the field does not
basics
~20 sTwo independent triggers attach the header: a request whose response tainting is cors, and a request whose method is neither GET nor HEAD. The second fires even when the destination is the sending host itself, so presence proves nothing about direction — only the value does.
solid answer
~40 sA browser attaches `Origin` on two independent triggers. The first is response tainting `"cors"` — a request made in a mode where the response may only be read under a grant. The second is **method**: in the ordinary case, any request whose method is neither `GET` nor `HEAD` carries the header regardless of where it is going. A form POST from the board to the board's own host therefore arrives stamped `Origin: https://board.example.org`, naming the receiving host itself. So the field's presence is not a "this came from somewhere else" signal — and its absence is not a same-origin signal either, since a same-host `GET` carries none. The information lives in the value, compared against the receiving host's own origin.
code
http · 6 linesPOST /admin/board-messages HTTP/1.1
Host: board.example.org
Origin: https://board.example.org
Content-Type: application/x-www-form-urlencoded
stop=42&text=Lift+out+of+servicego deeper
Know that the field is not exclusive to cross-origin traffic: a state-changing request back to the page's own host carries one too. Read the value, not the field's existence.
State both triggers and keep them separate: response tainting cors, and a method other than GET or HEAD. Then explain why safe methods are exempt — volume, and no state change to attribute.
Show the operational consequence. A log filter or rule keyed on presence silently sweeps in same-host state-changing traffic, and one keyed on absence proves nothing, because two unrelated situations produce an empty field.
The design tension is worth naming: an identity field on every request would be a browsing-history leak, so the specification buys attribution only where state changes. Any policy built on this header inherits that partial coverage.
An arrivals board is embedded on many host pages and calls one departures host for data, but the board's own administrative screens post back to the host that served them. Those same-host posts show up in the request log carrying an `Origin` field, and the first reaction is that something is misrouted. Nothing is misrouted. The rule for attaching the header has two independent triggers, and only one of them is about crossing an origin boundary. ## The two triggers 1. **Response tainting is `"cors"`.** When a request is made in a mode where the response may be handed to script only under an explicit grant, the header goes on so the receiving host can decide whether to grant. This is the trigger everyone knows, and it is the one that is genuinely about direction. 2. **The method is neither `GET` nor `HEAD`.** In the ordinary case, a request with any other method carries the header no matter where it is going — including straight back to the host that served the page. The second trigger is not an accident of implementation. `GET` and `HEAD` are the methods a browser will emit from ordinary markup without any script at all, at enormous volume; stamping every one of them would attach an identity field to essentially all browsing traffic. Methods that change state are both rarer and the ones a receiving host most wants to be able to attribute, so those get the field unconditionally. ```http POST /admin/board-messages HTTP/1.1 Host: board.example.org Origin: https://board.example.org Content-Type: application/x-www-form-urlencoded ``` Here `Host` and `Origin` name the same host. This request never crossed an origin boundary, and it still carries the field. ## What follows for the receiving host | Observation | What it actually licenses | |---|---| | The field is present | Either the request was made in a mode needing a grant, **or** its method was not `GET`/`HEAD` — you cannot tell which from presence alone | | The field is absent | Possibly a same-host `GET` or `HEAD`, possibly a client that is not a browser at all | | The field equals your own origin | The initiating context was your own origin, as the browser serialized it | | The field is some other origin | The initiating context was that one, as the browser serialized it | The first two rows are the ones that get answered wrongly in interviews. "The header is there, so this is cross-origin" is false because of trigger two. "The header is missing, so this is same-origin" is false because safe methods never carry it — and because the header is stamped by a browser, so a request that no browser made carries whatever its sender decided to carry, including nothing. ## Reading it correctly The information is in the **value**, not in the field's existence: - Compare the arriving value against the receiving host's own serialized origin. Equal means the initiating context was the same origin; different means it was a different one. - Remember that the comparison is over bytes, so the receiving host's own origin has to be written the same canonical way — scheme, host, non-default port only. - Treat a *missing* field as an absence of information rather than as evidence, because two very different situations produce it. ## Why this trips people up The mental model most engineers build is "the browser adds this header when it is calling someone else", which is exactly half the rule and the half that produces confident wrong answers. It also explains a common report from operators: a log filter written as "requests carrying an Origin field" intended to isolate embedded-board traffic ends up including every administrative post made from the host's own pages, because those posts satisfy trigger two. The repair is one sentence long: **presence is a fact about the request's method and mode; direction is a fact about the value.** Once that is separated, the same-host `POST` stops looking like a bug and the missing field on a same-host `GET` stops looking like one either.
- A same-host `GET` from the board carries no `Origin` field. Does that mean it is trustworthy?It means neither trigger fired: the request was not made in a mode needing a grant, and `GET` is one of the two exempt methods. It is not evidence about the caller. A request that no browser produced can also carry no field, and a request that no browser produced can equally carry any value its sender chose.
- Why are `GET` and `HEAD` the two methods left out?They are the methods a browser emits from ordinary markup with no script involved, and they dominate traffic by volume. Stamping them would attach an initiating-context identity to nearly all browsing. The methods that change state are rarer and are exactly the ones a receiving host wants attributable, so those carry the field unconditionally.
- An operator filters the log for requests carrying the field, expecting to isolate the embedded boards. What do they get?Everything the embeds sent, plus every state-changing request the host's own pages made to it — the second trigger does not care about destination. Filtering on presence answers a question about method and mode; to isolate other origins, the filter has to compare the value against the host's own serialized origin.
saying these in an interview costs you the question
- Reads any Origin field as proof the request was cross-origin
- Says a browser attaches it only on cross-origin requests
- Treats a missing Origin field as proof of same-origin
- Expects a same-host GET to carry the field
- Thinks a request made outside a browser cannot carry one