Skip to content

security: Host/Origin allowlist bypass via prefix match (startswith base + ':') in transport_security #3364

Description

@sloemo01

Security consideration: the SDK's SECURITY.md asks for private-advisory reports, but those require repo admin rights unavailable to outside reporters — filing publicly here so this is tracked (maintainers: happy to provide details privately on request).

Bug: Host/Origin allowlist bypass via prefix (startswith(base + ":")) matching

In src/mcp/server/transport_security.py (verified in mcp 2.0.0), both _validate_host and _validate_origin accept an allowed entry using a prefix match:

if allowed.endswith(":*"):
    base_host = allowed[:-2]
    if host.startswith(base_host + ":"):
        return True

Because this is a raw startswith(base + ":"), any header value that merely begins with the allowed host + : is accepted — the value after the colon is never validated as a real port.

Impact

  • With FastMCP's baked-in loopback defaults, allowed_hosts includes 127.0.0.1:* / localhost:*. A Host/Origin of 127.0.0.1:80.evil.com (or 127.0.0.1:anything.evil.com) is accepted, even though it is not the loopback host — defeating the DNS-rebinding / Cross-Origin restriction the middleware is meant to enforce.
  • Any user-supplied host entry (e.g. 172.21.12.48:*) has the identical looseness.
  • The suffix isn't even required to be a numeric port (127.0.0.1:evil.com passes).

Repro

  1. Server with enable_dns_rebinding_protection=True, allowed_hosts=["127.0.0.1:*"] (FastMCP's default for loopback).
  2. Send request with Host: 127.0.0.1:evil.com.
  3. Middleware accepts it (returns None) instead of 421.

Suggested fix

Validate the suffix is a numeric port only — e.g. host[len(base_host)+1:].isdigit() — or parse scheme+host+port properly (Starlette's TrustedHostMiddleware is a good reference). Apply the same fix to _validate_origin. This should also be gated so 127.0.0.1:evil.com (non-numeric) and 127.0.0.1:8080.evil.com are rejected.

Context

Found while reviewing repowise-dev/repowise#1737 — repowise correctly inherits FastMCP's existing loopback allowlist, so this is an upstream SDK issue, not a repowise defect. (Note: allowed_hosts wildcard subdomain support is already tracked in #2141; this issue is the distinct prefix-match bypass of the :* handling.)

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