skip to content

Your server refuses any WebSocket upgrade whose Origin field is not allowlisted, yet a scripted non-browser client connects freely. Why?

level: middleimportance: should knowfreq 46%

answer

  1. only one kind of client cannot lie
  2. the page never sets it
  3. free text to everything else
  4. ambient credentials are the real target
  5. cross-site defence, never identity

basics

~20 s

Only a browser is obliged to fill in the Origin request field honestly. Any other client writes whatever string it likes, so the check is a defence against pages the victim's browser loads, not authentication of the caller.

solid answer

~40 s

The `Origin` request field on a WebSocket handshake is trustworthy for exactly one reason: on a browser the field is set by the browser, not by the page, so a page cannot lie about which origin it came from. A command-line or server-side client composes the whole handshake itself and simply types the allowlisted value into the field, which costs nothing. So `Origin` validation stops a page the victim's browser loaded from spending the victim's ambient credentials — a cross-site defence — and it identifies nobody. Whether that connection is allowed at all still has to come from a credential the server verifies. Both properties hold at once, and answering only the first is the usual miss.

code

http · 12 lines
http
GET /occupancy HTTP/1.1
Host: board.example
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://console.example

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

go deeper

for a junior

Know that the browser, not the page, writes the Origin field on a WebSocket handshake, and that a client written by hand can put anything there.

for a middle

Explain the split cleanly: a defence against pages using the victim's ambient credentials on one side, no statement about the caller's identity on the other, and both true at once.

for a senior

Make the missing-field policy explicit per endpoint and keep one allowlist compared whole-string, then show that the credential check still runs on every connection regardless of what the field said.

for a principal

Own the rule across services: decide which endpoint classes consult the field at all, so teams do not each invent a policy and land a wildcard in one of the copies.

## Who fills the field in When a browser opens a WebSocket, it composes the handshake itself: the `GET` line, `Host`, `Upgrade: websocket`, `Connection: Upgrade`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`, and — because `RFC 6455` requires it of browser clients — `Origin`, set to the origin of the document that asked for the socket. The page supplies a URL and nothing else. It cannot reach into the handshake and change `Origin`, which is precisely why a server can draw a conclusion from it. A client that is not a browser has no such constraint. It assembles the request bytes itself, so `Origin` is just another line it can type, and the specification treats it as optional for non-browser clients. A script, a load generator, a service-to-service client or someone at a terminal will send `Origin: https://console.example` as easily as omitting it. Nothing on the wire distinguishes that request from the real console's. ## Cross-site defence versus authentication The distinction an interviewer is fishing for is what the check actually removes from the threat model: - **What it stops:** a page the victim's browser loads using the victim's **ambient** credentials — the cookie already in the browser, attached automatically. The attacker never sees that cookie; it is spent on their behalf. Refusing the handshake on `Origin` takes that away, because the attacker cannot make the victim's browser lie about where the page came from. - **What it does not stop:** anyone holding a credential, or probing without one, from a client of their own. They set the field and proceed. If your endpoint is reachable and your credential check is weak, the allowlist bought you nothing against them. | threat | blocked by Origin validation? | why | |---|---|---| | attacker's page in the victim's browser | yes | the browser sets the field, the page cannot | | a stolen cookie replayed from a script | no | the script types any origin it likes | | an unauthenticated probe from a terminal | no | same — the field is free text to it | | a network attacker reading frames | no | that is what transport encryption is for | ## Two decisions this forces **What an absent field means.** Because non-browser clients may omit `Origin` entirely, some share of legitimate traffic on many endpoints arrives without it. Default-allowing a missing field turns the whole check into a single line an attacker deletes. Default-denying it breaks every legitimate non-browser client. Neither is automatically right — the answer is per endpoint, and it should be written down: an operator-console socket can require the field and refuse without it, while a machine-to-machine endpoint should not consult it at all and should lean entirely on its credential check. **Where the allowlist lives.** One list, compared whole-string, applied in the handshake path. Origins tend to multiply — a staging console, a preview host, a second municipal department — and the moment the list is maintained in several places one copy grows a wildcard. An allowlist with a wildcard entry is, for this purpose, no allowlist at all. ## Why candidates get this backwards The field is named after a security concept and appears in a security-shaped position, so it is read as an identity claim. It is closer to a *provenance hint that only one kind of client is bound to tell the truth about*. The honest summary is two sentences long, and both halves are needed: 1. `Origin` validation is the defence against a socket opened by a page the user did not intend to grant access to; 2. it says nothing about who the client is, and every non-browser client can forge it. A useful phrasing to keep in your pocket: the field answers the question *which page asked for this socket*, and it answers it honestly only when a browser is the one filling it in. It never answers *who is on the other end*, and no amount of allowlist tightening turns the first question into the second. On the occupancy board, that means the server keeps both gates. The `Origin` allowlist refuses a handshake from anywhere but the operator console, so a stray page the operator visits cannot ride their session. The credential check runs regardless, so the field-less connection from the city's own reporting job is allowed or refused on what it presents, not on a string it could have typed. Remove either gate and one whole class of caller walks in.

  • If Origin cannot authenticate, what is it still worth keeping for?
    It removes the whole cross-site class: an unrelated page the operator visits cannot open a privileged socket on the strength of a cookie the browser attaches automatically. That attack needs no credential theft and no code on your site, so cutting it off for one string comparison is cheap.
  • Should a missing Origin field be allowed?
    Decide per endpoint and write it down. An operator-facing socket can require the field and refuse an upgrade without it; a machine-to-machine endpoint should ignore the field entirely and rely on its credential check. Silently defaulting to allow makes the whole check one deleted line away from nothing.

saying these in an interview costs you the question

  • Treats an allowlisted Origin as proof of who the client is
  • Thinks a page can choose the Origin its browser sends
  • Says any client can forge it, so the check is worthless
  • Allows a missing Origin field by default without deciding so
  • Keeps several copies of the allowlist, one with a wildcard