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
Copy link
Contributor

@maciej-kisiel maciej-kisiel left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! I added a few comments, but generally the approach looks sound.

I believe we should also add this protection to the legacy SSE transport.

// DNS rebinding protection: auto-enabled for localhost servers.
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
if !h.opts.DisableLocalhostProtection {
if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

With this change below I think we could only have a single implementation of the isLocalhost function, since they are largely the same. It would be called with localAddr.String() and req.Host.

Suggested change
if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
if localAddr := req.Context().Value(http.LocalAddrContextKey).(net.Addr); localAddr != nil {

disableProt bool // DisableLocalhostProtection setting
wantStatus int
}{
// Auto-enabled for localhost listeners (127.0.0.1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Extending the test cases to multiple lines and including field names is more readable. I would suggest to do it.

name string
listenAddr string // Address to listen on
hostHeader string // Host header in request
disableProt bool // DisableLocalhostProtection setting
Copy link
Contributor

Choose a reason for hiding this comment

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

disableProtection should be fine.

}
handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, opts)

// Create a listener on the specified address to control LocalAddrContextKey
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Create a listener on the specified address to control LocalAddrContextKey
// Create a listener on the specified address to control LocalAddrContextKey.

}
defer listener.Close()

// Start server in background
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Start server in background
// Start the server in the background.

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