You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Since 2.0 (#2286), createMcpExpressApp, createMcpHonoApp and createMcpFastifyApp turn on Origin validation by default for localhost-class binds, alongside the existing Host validation.
This blocks browser-based MCP clients such as extensions. A Chrome extension sends Origin: chrome-extension://<id>. The same request with the same credentials gets 200 without an Origin header and 403 with one. The only fix is for every server operator to allowlist each client by hand.
The default doesn't add rebinding protection. The DNS-rebinding advisory (GHSA-w48q-cv73-mx4w) is closed by the Host check, and the v2 docs already say that on a localhost bind the Host check is what stops rebinding. An Origin check is a CSRF control, and it only matters when the server accepts credentials the browser sends automatically, such as cookies.
The enableoriginverification setting, which could bring the old default back, was removed in 1.8.0.
docs/rough_edges.md says the option "should not have been part of the SDK API" because cross-origin protection "is a general HTTP concern, not specific to MCP".
Proposal
The app factories apply originValidation(allowedOrigins) only when allowedOrigins is passed. They no longer arm localhostOriginValidation() by default.
Host validation defaults are unchanged.
The Origin helpers stay exported, and so does the allowedOrigins option. Operators who want the 2.0 behaviour pass allowedOrigins: localhostAllowedOrigins().
Update the docs to describe allowedOrigins as the CSRF control and Host validation as the DNS-rebinding control.
v1.x needs no change, since its factory already defaults to Host-only.
This doesn't reopen CSRF on unauthenticated localhost servers. The transport already returns 415 for any POST whose Content-Type isn't application/json, and a cross-site JSON POST forces a CORS preflight that the server doesn't approve. I'd add a test that pins this.
Compatibility
This relaxes a 2.x default. A localhost server that relied on the automatic Origin check would now accept cross-origin requests that pass the Host check and the Content-Type check. Should this ship in a minor release with a changeset note, or wait for the next major? I'll go with the maintainers' call.
Further proposals
1. Scheme wildcards in allowedOrigins. Once Origin validation is opt-in, an operator who turns it on still has to list every browser-extension client by hand. That's impossible for Firefox, whose moz-extension://<uuid> origin differs on every install. Proposal:
An entry of the form <scheme>://* matches any Origin with that scheme, for example chrome-extension://* or moz-extension://*.
Existing hostname entries keep working as today.
http://* and https://* throw at construction time, because they would allow every website. Leaving allowedOrigins unset already does that.
null and unparseable Origins are still rejected.
2. A machine-readable reason in the 403 body.validateHostHeader and validateOriginHeader already compute an errorCode (missing_host, invalid_host_header, invalid_host, invalid_origin_header, invalid_origin), but the response only carries a free-text message. A client can't tell a Host or Origin rejection from an auth failure, so it can't tell the user that signing in again won't help. Proposal: include the code in the JSON-RPC error, keeping the status and message unchanged:
Problem
Since 2.0 (#2286),
createMcpExpressApp,createMcpHonoAppandcreateMcpFastifyAppturn on Origin validation by default for localhost-class binds, alongside the existing Host validation.This blocks browser-based MCP clients such as extensions. A Chrome extension sends
Origin: chrome-extension://<id>. The same request with the same credentials gets 200 without anOriginheader and 403 with one. The only fix is for every server operator to allowlist each client by hand.The default doesn't add rebinding protection. The DNS-rebinding advisory (GHSA-w48q-cv73-mx4w) is closed by the Host check, and the v2 docs already say that on a localhost bind the Host check is what stops rebinding. An Origin check is a CSRF control, and it only matters when the server accepts credentials the browser sends automatically, such as cookies.
The full threat model and SDK comparison are in modelcontextprotocol/modelcontextprotocol#3370.
Prior art: Go SDK
The Go SDK made exactly this change:
Hoston loopback) has been on by default since 1.4.0.CrossOriginProtectionmeans no check.enableoriginverificationsetting, which could bring the old default back, was removed in 1.8.0.docs/rough_edges.mdsays the option "should not have been part of the SDK API" because cross-origin protection "is a general HTTP concern, not specific to MCP".Proposal
originValidation(allowedOrigins)only whenallowedOriginsis passed. They no longer armlocalhostOriginValidation()by default.allowedOriginsoption. Operators who want the 2.0 behaviour passallowedOrigins: localhostAllowedOrigins().allowedOriginsas the CSRF control and Host validation as the DNS-rebinding control.This doesn't reopen CSRF on unauthenticated localhost servers. The transport already returns 415 for any POST whose Content-Type isn't
application/json, and a cross-site JSON POST forces a CORS preflight that the server doesn't approve. I'd add a test that pins this.Compatibility
This relaxes a 2.x default. A localhost server that relied on the automatic Origin check would now accept cross-origin requests that pass the Host check and the Content-Type check. Should this ship in a minor release with a changeset note, or wait for the next major? I'll go with the maintainers' call.
Further proposals
1. Scheme wildcards in
allowedOrigins. Once Origin validation is opt-in, an operator who turns it on still has to list every browser-extension client by hand. That's impossible for Firefox, whosemoz-extension://<uuid>origin differs on every install. Proposal:<scheme>://*matches anyOriginwith that scheme, for examplechrome-extension://*ormoz-extension://*.http://*andhttps://*throw at construction time, because they would allow every website. LeavingallowedOriginsunset already does that.nulland unparseable Origins are still rejected.2. A machine-readable reason in the 403 body.
validateHostHeaderandvalidateOriginHeaderalready compute anerrorCode(missing_host,invalid_host_header,invalid_host,invalid_origin_header,invalid_origin), but the response only carries a free-textmessage. A client can't tell a Host or Origin rejection from an auth failure, so it can't tell the user that signing in again won't help. Proposal: include the code in the JSON-RPC error, keeping the status and message unchanged:{ "jsonrpc": "2.0", "error": { "code": -32000, "message": "Invalid Origin: evil.example", "data": { "reason": "invalid_origin" } }, "id": null }Disclosure: drafted with Claude Code;