Skip to content

Conversation

@pcarleton
Copy link
Member

@pcarleton pcarleton commented Jan 22, 2026

Summary

Add DNS rebinding protection that is automatically enabled when requests arrive via localhost (127.0.0.1, [::1]). This protects against malicious websites using DNS rebinding to interact with local MCP servers.

(note: this was a claude assisted PR, mostly wanted to see how difficult passing this test would be for this SDK)

Design Goal: Secure by Default

The primary goal is to make it difficult to run a localhost server without these protections by mistake. There are other approaches that could provide secure defaults (e.g., a helper for ListenAndServe that does the localhost check), but using http.LocalAddrContextKey for runtime detection seemed like the most backwards compatible approach and least likely to be disabled by accident.

With this implementation:

  • No code changes required - existing servers get protection automatically
  • No opt-in needed - protection activates based on the connection's local address
  • Explicit opt-out - users must deliberately set DisableLocalhostProtection: true

Changes

  • Add DisableLocalhostProtection option to StreamableHTTPOptions
  • Add isLocalhostAddr and isLocalhostHost helper functions
  • Validate Host header at start of ServeHTTP, rejecting non-localhost Host headers with 403 Forbidden

How it works

The protection uses http.LocalAddrContextKey to detect the connection's local address at runtime. When a request arrives via localhost (127.0.0.1 or [::1]), the handler validates that the Host header also matches a localhost value. If not, the request is rejected with 403 Forbidden.

This approach means:

  • Protection is enabled for requests arriving via localhost, regardless of whether the server listens on 127.0.0.1 or 0.0.0.0
  • Requests arriving via non-localhost IPs (e.g., external network requests) are not affected

Edge case: Reverse proxies

If a reverse proxy (e.g., Envoy, nginx) runs on the same host and forwards requests to the MCP server via localhost while preserving the original Host header, those requests would be rejected. In this case, users should either:

  1. Set DisableLocalhostProtection: true
  2. Configure the proxy to rewrite the Host header to localhost

Testing

  • Added unit tests for isLocalhostAddr and isLocalhostHost helper functions
  • Added integration tests for the full protection flow
  • Verified against the MCP conformance test suite:
    • localhost-host-rebinding-rejected: PASS
    • localhost-host-valid-accepted: PASS

Related

Add DNS rebinding protection that is automatically enabled when requests
arrive via localhost (127.0.0.1, [::1]). This protects against malicious
websites using DNS rebinding to interact with local MCP servers.

Key changes:
- Add DisableLocalhostProtection option to StreamableHTTPOptions
- Add isLocalhostAddr and isLocalhostHost helper functions
- Validate Host header at start of ServeHTTP, rejecting non-localhost
  Host headers with 403 Forbidden when the connection arrives via localhost

The protection is enabled by default with no code changes required.
Users can opt-out by setting DisableLocalhostProtection: true.

This uses http.LocalAddrContextKey to detect the connection's local
address, which means protection is enabled for any request arriving
via localhost, regardless of whether the server listens on 127.0.0.1
or 0.0.0.0.

Closes: relates to MCP spec security best practices
See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
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.

1 participant