# How the MCP HTTP Transport Prevents DNS Rebinding Attacks

> Learn how the MCP HTTP transport prevents DNS rebinding attacks using strict Host and Origin header validation with the LoopbackOriginGuard ASGI middleware. Secure your applications today.

- Repository: [Tirth Kanani/code-review-graph](https://github.com/tirth8205/code-review-graph)
- Tags: how-to-guide
- Published: 2026-08-18

---

**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`](https://github.com/tirth8205/code-review-graph/blob/main/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/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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py#L37-L41):

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py#L48-L55) and [`http_origin_guard.py#L66-L70`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py#L66-L70):

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py#L72-L86):

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py#L206-L212):

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/main.py#L1155-L1161) wires everything together:

```python
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:

```python
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)](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`](https://github.com/tirth8205/code-review-graph/blob/main/tests/test_http_origin_guard.py#L56-L88), representative assertions include:

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/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.