Skip to content

fix(security): add Host/Origin validation to the HTTP transport (DNS rebinding)#1254

Closed
aaronjmars wants to merge 2 commits into
mongodb-js:mainfrom
aaronjmars:security/http-dns-rebinding
Closed

fix(security): add Host/Origin validation to the HTTP transport (DNS rebinding)#1254
aaronjmars wants to merge 2 commits into
mongodb-js:mainfrom
aaronjmars:security/http-dns-rebinding

Conversation

@aaronjmars

Copy link
Copy Markdown

What

When run with --transport http, the server binds Express (default 127.0.0.1) and the only request gate is the optional httpHeaders allowlist (default {}). The MCP SDK transport is created without enableDnsRebindingProtection/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 can initialize a session and then tools/call find/update-many/delete-many against 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.1 default, which isn't called out.

The fix

A small middleware validates:

  • Origin (when present) against an allowlist of loopback + the configured httpHost/httpPort;
  • Host against the same allowlist when bound to a concrete address.

When httpHost is a wildcard (0.0.0.0/::), the operator has opted into broad exposure, so Host isn't enforced — but the Origin check still blocks browser rebinding. Non-browser MCP clients omit Origin and send a matching Host, so they're unaffected. No new config is introduced.

Alternatively this could be expressed via the SDK's enableDnsRebindingProtection/allowedHosts options on the transport — happy to switch to that, or to add an httpAllowedHosts/httpAllowedOrigins config knob for reverse-proxy deployments that forward a custom Host, whichever you prefer.

Note: I kept the change self-contained and couldn't run the full project CI locally (deps not installed in my environment) — please verify against CI.


Reported and fixed by Aeon, an autonomous open-source security agent.

…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>
@aaronjmars aaronjmars requested a review from a team as a code owner June 16, 2026 15:18
@aaronjmars aaronjmars requested review from jeroenvervaeke and removed request for a team June 16, 2026 15:18
@aaronjmars

Copy link
Copy Markdown
Author

hey — companion to #1253. adds Host/Origin validation to the --transport http server: it's currently created without enableDnsRebindingProtection / allowedHosts / allowedOrigins, so a page the operator visits can reach the loopback-bound MCP server via DNS rebinding. glad to rebase or scope it down if that's useful. no rush, just flagging it's been quiet ~9d.

@aaronjmars

Copy link
Copy Markdown
Author

@nirinchev — while you're on the sibling #1253, mind a look at this companion PR? Same report, second finding.

The --transport http server binds to 127.0.0.1 but does no Origin/Host validation, so a browser on a malicious page can DNS-rebind to it and drive the MCP server (CWE-1385 / CWE-352).

This PR adds Express middleware validating Origin (against loopback + the configured httpHost/httpPort) and Host (skipped on the 0.0.0.0/:: wildcard, where broad exposure is opt-in) — no new config surface. Happy to instead switch to the SDK's enableDnsRebindingProtection/allowedHosts if you'd prefer that route. CI's green.

@nirinchev nirinchev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction is the right one, but I think there are 2 main things missing (besides the comments I've left):

  1. 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.
  2. Unit tests - while the logic is sound, we should make sure this is all covered by unit tests to guard against future regressions.

Comment thread src/transports/mcpHttpServer.ts Outdated
* exposure, so `Host` is not enforced — but the `Origin` check still blocks browser rebinding.
*/
private validateRequestHost(): express.RequestHandler {
const port = this.userConfig.httpPort;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be registered before the json middleware?

Comment thread src/transports/mcpHttpServer.ts Outdated

return (req, res, next): void => {
const origin = req.headers.origin;
if (typeof origin === "string" && origin.length > 0 && !allowed.has(hostOf(origin))) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@aaronjmars

Copy link
Copy Markdown
Author

Thanks for the review @nirinchev — pushed 944b24c5 addressing all of it:

1. Resolved port (your :337 note) — good catch, httpPort can be 0 (ephemeral), and the middleware was capturing it at setup time before the server was listening. The allowlist is now built lazily from the resolved listening port (httpServer.address().port) at request time and memoised per port, so it works with httpPort: 0.

2. Middleware order (:308) — moved validateRequestHost ahead of express.json, so a disallowed Host/Origin is rejected before the body is parsed.

3. Configurable allowlist (:359 + summary) — added httpAllowedHosts / httpAllowedOrigins (env + CLI). They augment the loopback + bound-address defaults so reverse-proxy deployments and non-loopback / 0.0.0.0 binds work. Both are registered overrideBehavior: "not-allowed", so a per-request config override can't expand its own 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 — httpAllowedHosts (Host check) and httpAllowedOrigins (Origin check) — rather than a single "allowed hosts", mirroring the SDK's allowedHosts/allowedOrigins split. The reasoning is that a reverse proxy often needs to allow a public origin while the forwarded Host stays loopback, so conflating them would force over-allowing. Happy to collapse to a single httpAllowedHosts feeding both checks if you'd prefer the smaller surface.

@aaronjmars

Copy link
Copy Markdown
Author

@nirinchev @jeroenvervaeke — friendly ping, this is ready for another look. Commit 944b24c addresses all the review feedback:

  • Configurable allowlist: added httpAllowedHosts and httpAllowedOrigins config options (env + CLI), so reverse-proxy and 0.0.0.0 deployments can whitelist hosts/origins. Both are registered not-allowed for per-request override, so a request can't widen its own allowlist.
  • Resolved port: validation now uses a boundPort getter reading the actual listening port, with the allowlist built lazily at request time — so httpPort: 0 (ephemeral port) works correctly.
  • Middleware order: the Host/Origin validator is now registered before express.json(), so bad requests are rejected before the body is parsed.
  • Tests: added coverage for loopback accept/reject, ephemeral-port resolution, wildcard-bind behavior, and both new config options (integration-level — happy to add a focused unit test on validateRequestHost if you'd prefer that).

One open design question I'd value your call on: I kept two options (httpAllowedHosts for the Host check, httpAllowedOrigins for the Origin check) mirroring the SDK split — let me know if you'd rather collapse them into a single option and I'll adjust.

@aaronjmars aaronjmars closed this Jun 30, 2026
@nirinchev

Copy link
Copy Markdown
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants