How the MCP HTTP Transport Prevents DNS Rebinding Attacks

The MCP HTTP transport prevents DNS rebinding attacks by enforcing strict Host and Origin header validation through a dedicated ASGI middleware called LoopbackOriginGuard, which rejects any request that does not originate from the same loopback address.

The code-review-graph repository implements a production-ready defense against DNS rebinding for its MCP (Model Context Protocol) HTTP server. When you run code-review-graph serve --http, the underlying FastMCP streamable-http transport binds to a loopback address such as 127.0.0.1:5555. Binding to localhost alone is insufficient—attackers can still use DNS rebinding to make browsers resolve malicious domains to 127.0.0.1 and send authenticated requests. This article examines the multi-layered protection mechanism implemented in http_origin_guard.py.

What Is a DNS Rebinding Attack Against Local Servers

A DNS rebinding attack exploits the browser's same-origin policy by rapidly switching DNS records. An attacker-controlled domain first resolves to their server, then flips to 127.0.0.0/8, allowing malicious JavaScript to send requests to your local MCP server as if same-origin. Lines 4-7 of [http_origin_guard.py](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py#L4-L7) document this threat explicitly, noting that browsers can resolve attacker domains to any loopback address.

The attack succeeds because:

  • The browser enforces same-origin based on the current DNS resolution
  • Local servers often lack authentication on the assumption that "localhost is safe"
  • Attacker code runs with full access to browser credentials and cookies

LoopbackOriginGuard: Core Defense Mechanism

The LoopbackOriginGuard class implements a two-header validation strategy. It operates as ASGI middleware, intercepting every HTTP request before FastMCP processing begins.

Conditional Activation on Loopback Binds

The guard only activates when the server binds to a loopback address. This prevents false positives when operators intentionally expose the service.

In http_origin_guard.py#L37-L41:

def __init__(self, host: str, port: int):
    self.host = host
    self.port = host
    self.enabled = is_loopback_host(host)  # True only for 127.x.x.x, ::1, etc.

If you bind to 0.0.0.0, self.enabled becomes False and the middleware passes all requests through.

Host Header Validation

Every request's Host header undergoes strict validation. The guard uses split_host_port to normalize the authority, then verifies:

  1. The host resolves to a loopback address
  2. The port matches the server's bound port

From http_origin_guard.py#L48-L55 and http_origin_guard.py#L66-L70:

def _authority_allowed(self, host: str, port: Optional[int]) -> bool:
    if not is_loopback_host(host):
        return False
    if port is not None and port != self.port:
        return False
    return True

async def __call__(self, scope, receive, send):
    if not self.enabled:
        await self.app(scope, receive, send)
        return
        
    headers = dict(scope["headers"])
    host_header = headers.get(b"host", b"").decode("ascii")
    host, port = split_host_port(host_header)
    
    if not self._authority_allowed(host, port):
        await self._reject(send, "Host validation failed")
        return

Origin Header Validation for Cross-Site Requests

Browser-initiated cross-origin requests include an Origin header. Non-browser MCP clients omit this header entirely. The guard exploits this distinction: it only rejects requests when Origin is present and violates the loopback constraint.

From http_origin_guard.py#L72-L86:

origin_header = headers.get(b"origin")
if origin_header:
    origin = origin_header.decode("ascii")
    # Parse scheme://host[:port]

    origin_host, origin_port = self._parse_origin(origin)
    
    # Implicit port handling: http → 80, https → 443

    if origin_port is None:
        origin_port = 443 if origin.startswith("https") else 80
        
    if not self._authority_allowed(origin_host, origin_port):
        await self._reject(send, "Origin validation failed")
        return

Middleware Integration in the HTTP Transport

The guard is not manually attached per-endpoint. Instead, build_http_middleware constructs a Starlette Middleware list that FastMCP applies globally.

In http_origin_guard.py#L206-L212:

def build_http_middleware(host: str, port: int) -> List[Middleware]:
    """Build middleware stack for the HTTP transport."""
    return [
        Middleware(
            LoopbackOriginGuard,
            host=host,
            port=port
        )
    ]

The CLI entry point in main.py#L1155-L1161 wires everything together:

middleware = build_http_middleware(args.host, args.port)
await mcp.run_http_async(
    transport="streamable-http",
    host=args.host,
    port=args.port,
    middleware=middleware,
)

This architecture ensures consistent protection across all MCP endpoints without requiring individual route handlers to implement security checks.

Complete Configuration Example

Here's how to configure the MCP HTTP transport with DNS rebinding protection manually:

from fastmcp import FastMCP
from code_review_graph.http_origin_guard import build_http_middleware

mcp = FastMCP("protected-server")

@mcp.tool
def analyze_code(file_path: str) -> dict:
    return {"status": "analyzed", "path": file_path}

# Bind to loopback with guard enabled

HOST, PORT = "127.0.0.1", 5555
middleware = build_http_middleware(HOST, PORT)

async def main():
    await mcp.run_http_async(
        transport="streamable-http",
        host=HOST,
        port=PORT,
        middleware=middleware,
    )

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Test Suite Verification

The protection is validated by [tests/test_http_origin_guard.py](https://github.com/tirth8205/code-review-graph/blob/main/tests/test_http_origin_guard.py), which covers attack vectors including:

  • Foreign Origin headers (https://evil.com)
  • Rebinding Host headers (malicious.local)
  • Wrong ports (127.0.0.1:8080 when server is on 5555)
  • Non-HTTP schemes (ftp://127.0.0.1)
  • IPv6 loopback variants ([::1])

From test_http_origin_guard.py#L56-L88, representative assertions include:

def test_rebinding_host_rejected(client):
    """DNS rebinding via Host header is blocked"""
    response = client.post(
        "/mcp/",
        json={"method": "tools/list"},
        headers={"Host": "attacker.example"}
    )
    assert response.status_code == 403

def test_foreign_origin_rejected(client):
    """Cross-origin requests from external domains are blocked"""
    response = client.post(
        "/mcp/",
        json={"method": "tools/list"},
        headers={"Origin": "https://evil.com"}
    )
    assert response.status_code == 403

Attack Scenario Walkthrough

Consider a complete rebinding attack and how the guard neutralizes it at each stage:

  1. Attacker sets up evil.com with DNS TTL of 0 seconds
  2. Victim visits evil.com → resolves to attacker's server (establishes same-origin)
  3. DNS record flips to 127.0.0.1
  4. Malicious JavaScript sends fetch("http://127.0.0.1:5555/mcp/")
  5. Browser includes Origin: https://evil.com (cross-origin)
  6. LoopbackOriginGuard detects Origin header points to evil.com
  7. Validation fails → HTTP 403 response, request never reaches FastMCP

Without Origin validation, the attack would succeed. Without Host validation, a rebinding attack using Host: evil.com instead would succeed.

Summary

  • Conditional activation: The LoopbackOriginGuard middleware only enforces checks when bound to loopback addresses, avoiding disruption of intentional external exposure.

  • Dual-header validation: Both Host and Origin headers are validated against the server's bound address and port, closing both direct and rebinding attack vectors.

  • ASGI middleware architecture: Protection applies uniformly across all endpoints without requiring per-route security code.

  • Comprehensive test coverage: The test suite verifies resistance against known DNS rebinding techniques and edge cases.

  • Transparent integration: The build_http_middleware helper and main.py wiring make the security mechanism automatic for CLI users.

Frequently Asked Questions

Does the guard protect against all localhost attacks?

No. The LoopbackOriginGuard specifically targets DNS rebinding attacks originating from browsers. It does not protect against malware already running on the same machine, reverse proxy misconfigurations, or attacks through non-HTTP protocols. The protection is scoped to the HTTP transport's threat model.

What happens if I bind to 0.0.0.0 instead of 127.0.0.1?

The guard automatically disables itself. As shown in http_origin_guard.py#L37-L41, self.enabled = is_loopback_host(host) returns False for non-loopback addresses. This assumes the operator intends external exposure and is responsible for their own access controls.

Can legitimate browser-based MCP clients use the HTTP transport?

Legitimate browser clients face the same restrictions as attackers—the Origin header must match the loopback origin. However, MCP is primarily designed for local tool invocation from desktop applications (IDEs, CLIs) that do not send Origin headers. Browser-based usage typically requires same-origin deployment or explicit non-loopback binding with external authentication.

Why validate both Host and Origin headers?

Host validation catches direct attacks where DNS rebinding makes the browser believe evil.com is localhost via the Host header. Origin validation catches cross-site request attacks where the browser correctly identifies the origin as external but the attacker hopes the server ignores it. Both are necessary because browsers behave differently depending on request type and caching state.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →