Skip to content

DNS rebinding protection defaults off when security_settings is omitted (non-loopback binds stay unprotected) | #3562

Description

@tiagovilasboas

Static review of public source at commit 6affe5c0d358. No traffic was sent to any MCP environment.

TransportSecurityMiddleware treats a missing settings argument as “protection off”, even though the settings model itself defaults enable_dns_rebinding_protection to True:

src/mcp/server/transport_security.py (model default + constructor):

enable_dns_rebinding_protection: bool = True
...
def __init__(self, settings: TransportSecuritySettings | None = None):
    # If not specified, disable DNS rebinding protection by default for backwards compatibility
    self.settings = settings or TransportSecuritySettings(enable_dns_rebinding_protection=False)

StreamableHTTPServerTransport passes that through unchanged (security_settings: ... = None → TransportSecurityMiddleware(security_settings) at the transport constructor).

MCPServer.streamable_http_app / sse_app only auto-enable an allowlist when host is loopback (127.0.0.1 / localhost / ::1) — see src/mcp/server/lowlevel/server.py around the auto-enable block. Binding or mounting with 0.0.0.0, a LAN IP, or a reverse-proxy hostname therefore ships without Host/Origin checks unless the operator remembers to pass TransportSecuritySettings explicitly.

DNS rebinding against a browser-reachable MCP HTTP transport is exactly what those checks are for. Fail-open on the common “I mounted the ASGI app / bound all interfaces” path is the surprising default.

Suggested change:

  • When security_settings is None, enable protection with a documented default allowlist (at least the bind host), or refuse to serve HTTP transports until settings are provided.
  • Keep an explicit opt-out (enable_dns_rebinding_protection=False) for demos that truly need it.
  • Log once at startup when protection is disabled.

Severity: medium as insecure default / defense-in-depth for HTTP transports; not claiming a working exploit against a specific deployment. No proof-of-concept.

Happy to send a focused PR if this direction is useful.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 22, 2026
  2. sattyamjjain commented on Sep 28, 2026

    @sattyamjjain

    Confirmed on main (f1b6589). transport_security.py L46-48 falls back to enable_dns_rebinding_protection=False when settings is None, and the auto-enable in lowlevel/server.py L742 and mcpserver/server.py L1155 only fires for 127.0.0.1 / localhost / ::1. v1.x fastmcp/server.py L191-195 has the same block.

    Two things to add:

    1. The bigger gap is people wiring StreamableHTTPSessionManager or SseServerTransport directly. That path never goes through the loopback auto-enable, so protection is off even on localhost unless they pass settings.

    2. "Allowlist the bind host" won't work for 0.0.0.0. Browsers send the public hostname, not the bind address, so there's nothing to derive. I think the safer default for a non-loopback host with no settings is to log a clear warning at startup (or refuse to start) until allowed_hosts is set, with an explicit opt-out for people behind a proxy that already checks Host.

    For context, I maintain agent-audit-kit, and its AAK-DNS-REBIND-001 rule flags StreamableHTTP/FastMCP network transports with no allowed_hosts. It doesn't yet check for enable_dns_rebinding_protection=False explicitly. I'll add that case.

  3. shleder commented on Sep 28, 2026

    @shleder

    Defaulting DNS rebinding protection to disabled when security_settings is omitted leaves local MCP servers vulnerable to intranet attacks.

    When an MCP server binds to 127.0.0.1 without validating the HTTP Host header, a malicious website visited in the developer's browser can execute DNS rebinding: resolving a domain to localhost after initial load and dispatching unauthenticated RPCs to the local MCP server. DNS rebinding protection (verifying that Host strictly equals localhost or 127.0.0.1 and checking Origin headers) should be fail-closed by default, requiring an explicit opt-out only for remote proxy deployments.

    In local agent sandboxes, pairing network namespaces (CLONE_NEWNET) with localhost egress filtering guarantees that even if an agent tool is compromised, local loopback services remain unreachable.


    Disclaimer: I am the author/maintainer of Vetto, an open-source daemon-less sandbox layer for AI coding agents.

  4. maxisbey commented on Oct 6, 2026

    @maxisbey
    Contributor

    Thanks for the review. I'm going to close this as not planned.

    The mounted-app case is already covered. streamable_http_app() and sse_app() default to host="127.0.0.1", so with no settings the protection is on and any request for a real hostname gets a 421. It's only off if you pass a non-localhost host= yourself, and Deploy & scale says to set transport_security= in that case.

    For a server bound to 0.0.0.0 there's no hostname we could build an allowlist from. Switching the low-level default on would also reject every request on existing 2.x deployments that build the transport directly.

    AI Disclaimer


    Generated by Claude Code

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

    v1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions