skip to content

Why must an MCP Streamable HTTP server validate the Origin header and return 403?

level: seniorimportance: should knowfreq 42%

answer

  1. the browser will POST anywhere it can reach
  2. one header the page cannot forge
  3. localhost is not a security boundary
  4. 403, not 401
  5. useless against a client that is not a browser

basics

~20 s

Because a web page can make the browser POST to any reachable HTTP endpoint, including one on localhost. MCP revision 2026-07-28 requires the server to validate the Origin header and answer an invalid one with 403, so a hostile page cannot drive a locally running MCP server's tools.

solid answer

~50 s

An MCP server speaking Streamable HTTP is reachable by anything that can make an HTTP request — including a browser executing a page the user did not write. If a server runs on the user's machine, a page loaded from any site can POST to `http://localhost:<port>` and, absent a check, invoke that server's tools with the machine's local authority: reading files, running commands, reaching internal networks. MCP revision 2026-07-28 therefore requires the server to validate the `Origin` header on incoming requests and to answer an invalid origin with HTTP `403`. `Origin` is one of the headers a browser sets itself and page script cannot forge, which is what makes it usable as a defence. It is not the whole answer: a local server should also bind to `127.0.0.1` rather than `0.0.0.0`, and remote servers still need real authorization, because a non-browser client can send any `Origin` it likes.

code

http · 8 lines
http
POST /mcp HTTP/1.1
Host: 127.0.0.1:8931
Origin: https://evil.example
Content-Type: application/json
Accept: application/json, text/event-stream

HTTP/1.1 403 Forbidden
Content-Length: 0

go deeper

for a junior

Know that an MCP HTTP server must check the Origin header and reply 403 when it is not one the server accepts.

for a middle

Explain the threat: a browser page can POST to a loopback MCP endpoint, and Origin is a header the browser sets that page script cannot forge, which is what makes the check meaningful.

for a senior

Show the layered picture: bind to 127.0.0.1, authenticate even locally, refuse browser preflight when no browser client is legitimate, and be explicit that Origin constrains browsers only.

for a principal

Own the threat model for how MCP servers get deployed on user machines — what local authority a server holds, what a hostile page could do with it, and what organizational defaults you mandate so that no MCP server ships listening on 0.0.0.0.

## The threat MCP servers are frequently run on the user's own machine and reached over HTTP on a loopback port. That machine is also running a browser, and the browser will execute whatever any visited page tells it to — including issuing a POST to `http://127.0.0.1:8931/mcp`. If the server does not check who asked, the consequences are severe, because a locally running MCP server typically holds exactly the authority an attacker wants: filesystem access, shell execution, credentials in a config file, reachability into a corporate network that the attacker's own machine cannot touch. The page never needs to read the response to do damage — a single `tools/call` that deletes, exfiltrates or reconfigures is enough. ## Why Origin is the header to check `Origin` is set by the browser, not by page script, and script cannot override it. That property is what makes it a usable signal: a request bearing `Origin: https://evil.example` is one the browser is telling you came from that site's page. MCP revision 2026-07-28 requires servers to validate it and specifies HTTP `403` as the answer when it is not acceptable. A local server's allow-list is typically tiny — often empty, because a local MCP server usually has no legitimate browser-page caller at all, only a native host application, which sends no `Origin`. Being strict costs nothing there. The rule also blunts DNS rebinding, where an attacker controls a hostname whose DNS answer flips to `127.0.0.1` after the page loads so that requests reach the loopback service while the browser still considers them same-origin by hostname. The origin the browser reports remains the attacker's, so an origin check refuses the request even though the network path succeeded. ## What Origin does not do This is the part interviewers push on. - **It is not authentication.** Any non-browser client — `curl`, a script, malware already running on the host — sets whatever `Origin` it wants. The check constrains *browsers*, which is precisely the attacker the loopback scenario involves, and constrains nothing else. - **It does not protect a remote server.** A public MCP server needs real authorization: MCP defines the server as an OAuth 2.1 resource server with audience-bound tokens and no token passthrough. Origin checking is a complement to that, never a substitute. - **It does not sanction the caller's intent.** An allowed origin still gets whatever the tools expose, so tool-level authorization and human consent still apply. ## The defences that go with it **Bind to the loopback interface.** A local server listening on `0.0.0.0` is reachable from the whole network segment — a coffee-shop Wi-Fi, a shared office VLAN — and no header check helps against a peer machine that simply omits `Origin`. Listening on `127.0.0.1` removes that reachability entirely. **Authenticate even locally.** A bearer token the host application knows and a random page does not turns a same-machine attack from trivial into infeasible. **Do not echo permissive CORS.** Returning `Access-Control-Allow-Origin: *` alongside a strict origin check is a contradiction that eventually gets resolved in the attacker's favour by a well-meaning refactor. If browsers are not legitimate callers, say so consistently. **Randomise the port and keep it out of logs.** Weak on its own, useful in depth: it denies an attacker a fixed target to scan. ## Interaction with preflight Because MCP requests carry custom headers such as `Mcp-Method` and a non-simple content type, a cross-origin browser request triggers a CORS preflight `OPTIONS` before the POST. A server that intends never to serve browser pages should simply not grant that preflight. A server that does intend to — a hosted remote server with a legitimate web client — must configure allowed origins and headers explicitly and narrowly, and remember that CORS protects the *browser's* view of the response, not the server: a request that has already reached your handler has already had whatever effect it was going to have. ## What a strong answer sounds like Name the requirement (validate `Origin`, `403` when invalid), name the threat model precisely (browser-driven requests to a locally reachable endpoint, DNS rebinding), and then name the limit out loud — that `Origin` is only meaningful against browsers, so loopback binding, local authentication and, for remote servers, OAuth-based authorization are what actually carry the weight.

  • Why is binding to 127.0.0.1 still necessary if the server checks Origin?
    Because `Origin` is only sent by browsers. A process on another machine on the same network can POST to a server listening on `0.0.0.0` with no `Origin` at all, or with any value it likes. Loopback binding removes the network reachability that makes that possible, so the two defences cover different attackers rather than overlapping.
  • Is 403 or 401 the right status for a rejected origin?
    `403`. `401` means "authenticate and try again", which is meaningless here — the request is refused because of where it came from, and no credential the page could supply changes that. MCP specifies `403` for an invalid origin. Reserve `401` and the OAuth challenge path for missing or insufficient tokens on a remote server.
  • Does CORS protect the MCP server itself?
    No. CORS governs whether the *browser* hands the response back to page script; the request has already reached your handler and any side effect has already happened. That is why an origin check must be a server-side rejection before the work runs, not a matter of which CORS headers you return.

saying these in an interview costs you the question

  • Treats binding to localhost as sufficient protection on its own
  • Believes Origin cannot be spoofed by any client
  • Returns permissive CORS headers alongside a strict origin check
  • Uses 401 for a rejected origin instead of 403
  • Assumes a browser page cannot reach a loopback port

context