You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Security: DNS rebinding protection is opt-in, not enabled by default #3784
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
Enable DNS rebinding protection by default
Or at minimum: log a warning when disabled
Document the risk prominently
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).
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.
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:
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.
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.
Activity
sattyamjjain commentedon Oct 29, 2026
I checked this against main (d7845c0), and the default is narrower than the title suggests.
TransportSecuritySettings.enable_dns_rebinding_protectiondefaults toTrue(src/mcp/server/transport_security.py L26).TransportSecuritySettings(enable_dns_rebinding_protection=False)(L47-48).streamable_http_appin src/mcp/server/lowlevel/server.py (L752-757) andMCPServer.sse_app(src/mcp/server/mcpserver/server.py L1163-1168) auto-enable it whenhostis 127.0.0.1, localhost or ::1 and notransport_securityis passed.So the exposed cases are servers that build
SseServerTransportorStreamableHTTPSessionManagerdirectly without settings, and servers bound to a non-loopback host such as 0.0.0.0 withouttransport_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
Originheader 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.
Ben-Home commentedon Oct 29, 2026
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:
Fail with a message that names the setting. If protection becomes default-on, installations behind a proxy that rewrites
Hostwill start returning 403s, and a bare rejection is indistinguishable from an auth failure. A response body or startup error that namesallowed_hostsand shows the receivedHostvalue 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.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=Falseexplicitly. 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
maxToolInputElementsbeing 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
Hostincluding 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 validatesOrigin, keep the two checks separate in the code so a deployment can tell which one rejected a request.