Skip to content

Security: DNS rebinding protection is opt-in, not enabled by default #3784

Open

Description

@alemaiorano

Security Configuration Recommendation

The TransportSecurityMiddleware in mcp/server/transport_security.py has DNS rebinding protection available but disabled by default:

self.settings = settings or TransportSecuritySettings(enable_dns_rebinding_protection=False)

Every deployment that does not explicitly pass TransportSecuritySettings(enable_dns_rebinding_protection=True, allowed_hosts=[...]) is vulnerable to DNS rebinding attacks.

Impact

DNS rebinding bypasses same-origin policy and allows malicious webpages to interact with local MCP servers. Most MCP servers run on localhost during development.

Recommendation

  1. Enable DNS rebinding protection by default
  2. Or at minimum: log a warning when disabled
  3. Document the risk prominently
  4. Consider a strict mode that enables all protections

Reference

Same class as GHSA-9h52-p55h-vw2f. Similar to TypeScript SDK maxToolInputElements being off by default (GHSA-46jr).

Verified in mcp 1.29.0 and 2.2.0 source.

Activity

  1. added
    enhancementRequest for a new feature that's not currently supported
    on Oct 28, 2026
  2. sattyamjjain commented on Oct 29, 2026

    @sattyamjjain

    I checked this against main (d7845c0), and the default is narrower than the title suggests.

    • TransportSecuritySettings.enable_dns_rebinding_protection defaults to True (src/mcp/server/transport_security.py L26).
    • The middleware's fallback, when it gets no settings, turns it off: TransportSecuritySettings(enable_dns_rebinding_protection=False) (L47-48).
    • But streamable_http_app in src/mcp/server/lowlevel/server.py (L752-757) and MCPServer.sse_app (src/mcp/server/mcpserver/server.py L1163-1168) auto-enable it when host is 127.0.0.1, localhost or ::1 and no transport_security is passed.

    So the exposed cases are servers that build SseServerTransport or StreamableHTTPSessionManager directly without settings, and servers bound to a non-loopback host such as 0.0.0.0 without transport_security. Also, a request with no Origin header passes _validate_origin (L74-76).

    The spec (2025-11-25, basic/transports) says "Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks". A middle ground could be to keep the loopback auto-enabled and log a warning, or refuse to start when the host is non-loopback and no settings were passed.

    I only read main, not the 1.29.0 tag.

  3. Ben-Home commented on Oct 29, 2026

    @Ben-Home

    The default is the whole finding, and it is worth making the argument in those terms because "opt-in" makes it sound like a preference rather than an insecure default.

    The population that is exposed is defined by omission: every server that never passes TransportSecuritySettings. That is not a small or careless group, it is the normal shape of a development server, where the security settings are simply not part of the code anyone writes. A control that only protects people who already thought about the threat is not protecting the threat.

    Two things would make a default-on change land cleanly, and both are about the failure mode rather than the setting:

    1. Fail with a message that names the setting. If protection becomes default-on, installations behind a proxy that rewrites Host will start returning 403s, and a bare rejection is indistinguishable from an auth failure. A response body or startup error that names allowed_hosts and shows the received Host value turns a support ticket into a one-line fix. A silent 403 after a patch upgrade is a worse outcome than the vulnerability for those users.

    2. Log the effective configuration at startup, not just the disabled case. A single line stating whether protection is enabled and which hosts are allowed makes the security posture auditable, and it catches the deployment where someone copied a settings object from an old example that sets enable_dns_rebinding_protection=False explicitly. Your recommendation covers warning when disabled; logging the value in both states is what makes it verifiable from outside the process.

    On the comparison you drew: the TypeScript SDK's maxToolInputElements being off by default is the same class in a different dimension, and it is worth citing together, because the pattern is what maintainers should act on rather than either instance. Both are protections that are correct and cheap when enabled, off when unset, and reachable by an attacker through ordinary use of a locally running server.

    One implementation detail if a PR follows, since it decides whether the protection actually holds: the check has to run on every request, including the initial SSE stream establishment and the streamable HTTP POSTs that follow, and it must parse Host including the port and the bracketed IPv6 form. Comparing lowercase hostnames against an allowlist without normalizing those forms produces a check that passes your test suite and misses real requests. If the same middleware also validates Origin, keep the two checks separate in the code so a deployment can tell which one rejected a request.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRequest for a new feature that's not currently supported

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions