fix(security): add Host/Origin validation to the HTTP transport (DNS rebinding)#1254
fix(security): add Host/Origin validation to the HTTP transport (DNS rebinding)#1254aaronjmars wants to merge 2 commits into
Conversation
…rebinding)
The streamable-HTTP transport binds Express (default 127.0.0.1) and the only
request gate is the optional `httpHeaders` allowlist (default `{}`). The MCP SDK
transport is constructed without `enableDnsRebindingProtection`/`allowedHosts`,
and there is no Origin/Host validation in the Express path. A web page can use
DNS rebinding to drive the operator's localhost-bound server (read/write to the
connected database) from the browser, since after rebinding the requests look
same-origin and skip CORS.
Add a small middleware that validates `Origin` (when present) and `Host` against
an allowlist derived from the configured `httpHost`/`httpPort` plus loopback.
Non-browser MCP clients omit `Origin` and send a matching `Host`, so they are
unaffected. When bound to a wildcard address (e.g. 0.0.0.0) the operator has
opted into broad exposure, so `Host` is not enforced — but the `Origin` check
still blocks browser-driven rebinding. No new configuration surface is added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
hey — companion to #1253. adds Host/Origin validation to the |
|
@nirinchev — while you're on the sibling #1253, mind a look at this companion PR? Same report, second finding. The This PR adds Express middleware validating |
nirinchev
left a comment
There was a problem hiding this comment.
The direction is the right one, but I think there are 2 main things missing (besides the comments I've left):
- We should expose an option to set the allowed hosts - that will enable reverse proxy deployments as well as binding to 0.0.0.0 and accepting requests from non-loopback origins.
- Unit tests - while the logic is sound, we should make sure this is all covered by unit tests to guard against future regressions.
| * exposure, so `Host` is not enforced — but the `Origin` check still blocks browser rebinding. | ||
| */ | ||
| private validateRequestHost(): express.RequestHandler { | ||
| const port = this.userConfig.httpPort; |
There was a problem hiding this comment.
The port here may not be yet resolved - e.g. if someone sets it to 0, a free random port will be chosen. We should use the resolved port rather than the one from the config.
|
|
||
| protected setupMiddlewares(): void { | ||
| this.app.use(express.json({ limit: this.userConfig.httpBodyLimit })); | ||
| this.app.use(this.validateRequestHost()); |
There was a problem hiding this comment.
Should this be registered before the json middleware?
|
|
||
| return (req, res, next): void => { | ||
| const origin = req.headers.origin; | ||
| if (typeof origin === "string" && origin.length > 0 && !allowed.has(hostOf(origin))) { |
There was a problem hiding this comment.
If we're enforcing the origin even if enforceHost is false, we should have a mechanism to set the allowed hosts and validate against those.
Follow-up to the DNS-rebinding protection, addressing review feedback: - Resolve the bound port at request time instead of capturing httpPort, which is 0 (ephemeral) until the server is listening; the allowlist is now built lazily and memoised per resolved port. - Run the validation middleware before express.json so a disallowed Host/Origin is rejected before the body is parsed. - Add httpAllowedHosts / httpAllowedOrigins config options so operators can permit reverse-proxy hostnames or a non-loopback / 0.0.0.0 bind. Both are non-overridable, so a request cannot expand its own allowlist. - Add unit/integration coverage for the loopback, wildcard-bind, ephemeral-port, and allowed-hosts/origins cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review @nirinchev — pushed 1. Resolved port (your 2. Middleware order ( 3. Configurable allowlist ( 4. Tests — added integration coverage for the loopback accept/reject paths, ephemeral-port resolution, wildcard bind (Host not enforced, Origin still blocked), and both new config options. One judgment call I'd like your read on: I exposed two options — |
|
@nirinchev @jeroenvervaeke — friendly ping, this is ready for another look. Commit
One open design question I'd value your call on: I kept two options ( |
|
Hey @aaronjmars sorry for the delay here - we've been at an offsite this week, so have been focused on other activities. I still think the changes here are valuable - if you don't have time to dedicate on this, that's totally fair, just wanted to let you know why we're not super responsive. |
What
When run with
--transport http, the server binds Express (default127.0.0.1) and the only request gate is the optionalhttpHeadersallowlist (default{}). The MCP SDK transport is created withoutenableDnsRebindingProtection/allowedHosts/allowedOrigins, and there's no Origin/Host validation in the Express middleware chain:https://github.com/mongodb-js/mongodb-mcp-server/blob/main/src/transports/mcpHttpServer.ts#L306-L319
Why it matters
A web page the operator visits can use DNS rebinding — a hostname that resolves to the attacker IP, then re-resolves to
127.0.0.1— to issue requests to the localhost-bound server. After rebinding the browser treats them as same-origin, so CORS doesn't help. The page caninitializea session and thentools/callfind/update-many/delete-manyagainst the operator's connected database.The README already notes HTTP transport shouldn't be exposed to a network without auth/proxy/firewall — but the browser-rebinding vector reaches even the safe-looking
127.0.0.1default, which isn't called out.The fix
A small middleware validates:
Origin(when present) against an allowlist of loopback + the configuredhttpHost/httpPort;Hostagainst the same allowlist when bound to a concrete address.When
httpHostis a wildcard (0.0.0.0/::), the operator has opted into broad exposure, soHostisn't enforced — but theOrigincheck still blocks browser rebinding. Non-browser MCP clients omitOriginand send a matchingHost, so they're unaffected. No new config is introduced.Alternatively this could be expressed via the SDK's
enableDnsRebindingProtection/allowedHostsoptions on the transport — happy to switch to that, or to add anhttpAllowedHosts/httpAllowedOriginsconfig knob for reverse-proxy deployments that forward a customHost, whichever you prefer.Reported and fixed by Aeon, an autonomous open-source security agent.