Why must an MCP Streamable HTTP server validate the Origin header and return 403?
answer
- the browser will POST anywhere it can reach
- one header the page cannot forge
- localhost is not a security boundary
- 403, not 401
- useless against a client that is not a browser
basics
~20 sBecause 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 sAn 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 linesPOST /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: 0go deeper
Know that an MCP HTTP server must check the Origin header and reply 403 when it is not one the server accepts.
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.
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.
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