# How the HTTP Transport Origin Guard Prevents DNS Rebinding Attacks

> Learn how the HTTP transport origin guard stops DNS rebinding attacks. It validates host and origin headers, blocking malicious requests with a 403 Forbidden response.

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

---

**The HTTP transport origin guard prevents DNS rebinding attacks by validating that the `Host` and `Origin` headers in every request resolve to the server's loopback address, rejecting any request with attacker-controlled hostnames with a 403 Forbidden response.**

The `code-review-graph` repository implements a security-focused MCP (Model Context Protocol) server that exposes tools over HTTP. When running locally with the `serve --http` command, the server binds to `127.0.0.1:5555` by default. This creates a DNS rebinding vulnerability: malicious websites can make browsers resolve `evil.example` to `127.0.0.1` and send requests to your local server. The **HTTP transport origin guard** in [`code_review_graph/http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py) neutralizes this threat through strict header validation.

## How DNS Rebinding Attacks Work

A DNS rebinding attack exploits the gap between **IP-level routing** and **application-level security**. An attacker controls a DNS record that first resolves to their server, then rapidly switches to `127.0.0.1`. A victim's browser, having already loaded scripts from the attacker's domain, now makes requests to the localhost server with the attacker's hostname in headers. Without validation, the local server accepts these requests as legitimate.

The code-review-graph guard closes this gap by verifying that requests originate from loopback addresses at the **semantic layer**—examining what the client claims about its origin rather than just where the packets came from.

## The LoopbackOriginGuard Architecture

The guard is implemented as **pure ASGI middleware** in [`code_review_graph/http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py). This design choice avoids buffering issues that plague `BaseHTTPMiddleware` subclasses when handling streaming responses or Server-Sent Events (SSE).

### Activation Condition: Loopback Detection

The guard only activates when the server binds to a loopback address. This preserves operator intent: if you bind to `0.0.0.0`, you are deliberately exposing the endpoint and the guard steps aside.

```python

# From build_http_middleware in http_origin_guard.py

enabled = is_loopback_host(host)  # True for 127.0.0.1, ::1, localhost

guard = LoopbackOriginGuard(
    app,
    host=host,
    port=port,
    enabled=enabled,  # Guard disabled for non-loopback binds

)

```

The `is_loopback_host()` helper uses Python's `ipaddress` module and special-cases `localhost`:

```python
def is_loopback_host(host: str) -> bool:
    if host == "localhost":
        return True
    try:
        return ipaddress.ip_address(host).is_loopback
    except ValueError:
        return False

```

### Request-Time Validation: Host Header Enforcement

When enabled, every request passes through `LoopbackOriginGuard.__call__()`. The middleware extracts the `Host` header and runs `_authority_allowed()` to parse and validate it:

```python
async def __call__(self, scope, receive, send):
    if scope["type"] != "http":
        return await self.app(scope, receive, send)
    
    headers = Headers(scope=scope)
    host = headers.get("host")
    
    if not self._authority_allowed(host):
        return await self._forbid(scope, receive, send)  # 403 Forbidden

    
    # ... Origin header check follows

```

The `_authority_allowed()` function uses `split_host_port()` to handle IPv4, IPv6 bracket notation (`[::1]`), and optional ports:

```python
def _authority_allowed(self, value: str, implicit_port: Optional[int] = None) -> bool:
    host, port = split_host_port(value)
    if not is_loopback_host(host):
        return False
    # Port must match server's bound port (or implicit default)

    effective_port = int(port) if port else implicit_port
    return effective_port == self.port

```

This is where DNS rebinding fails. A rebinding request arrives at `127.0.0.1` at the IP layer, but the `Host: evil.example` header contains a non-loopback hostname. The guard rejects it immediately.

### Defense in Depth: Origin Header Validation

For browser-based requests, the guard adds a second layer checking the `Origin` header. This catches cross-origin requests that might otherwise slip through:

```python
origin = headers.get("origin")
if origin:
    parsed = urlparse(origin)
    if parsed.scheme not in ("http", "https"):
        return await self._forbid(scope, receive, send)
    # Reuse same authority validation

    if not self._authority_allowed(
        parsed.netloc,
        implicit_port=80 if parsed.scheme == "http" else 443
    ):
        return await self._forbid(scope, receive, send)

```

Non-browser MCP clients typically omit `Origin`, so this check doesn't interfere with legitimate tool use.

## Integration with the HTTP Server

The guard wires into the FastMCP application through `build_http_middleware()`. When you run `code-review-graph serve --http`, the CLI entry point in [`main.py`](https://github.com/tirth8205/code-review-graph/blob/main/main.py) constructs the middleware stack:

```python

# Example: building the FastMCP app with the guard

from fastmcp import FastMCP
from code_review_graph.http_origin_guard import build_http_middleware

HOST = "127.0.0.1"
PORT = 5555

mcp = FastMCP("my-project")

@mcp.tool
def echo(msg: str) -> str:
    return msg

# build_http_middleware automatically includes LoopbackOriginGuard

app = mcp.http_app(middleware=build_http_middleware(HOST, PORT))

```

For custom ASGI applications, apply the guard directly:

```python
from starlette.applications import Starlette
from code_review_graph.http_origin_guard import LoopbackOriginGuard

inner_app = Starlette()
guarded_app = LoopbackOriginGuard(
    inner_app,
    host="127.0.0.1",
    port=5555,
)

# guarded_app is standard ASGI—use with uvicorn, hypercorn, etc.

```

## Test-Driven Security Guarantees

The test suite in [`tests/test_http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/tests/test_http_origin_guard.py) proves the guard's effectiveness against real attack vectors:

- **`test_rebound_host_is_rejected`** — A request with `Host: evil.example` (classic DNS rebinding payload) receives **403 Forbidden**
- **`test_foreign_origin_is_rejected`** — Cross-origin browser requests with `Origin: http://evil.example` are blocked
- **`test_same_origin_is_allowed`** — Legitimate loopback origins pass validation

These tests run against actual ASGI request cycles, ensuring the guard works in production conditions.

## Key Files and Functions

| File | Purpose |
|------|---------|
| [`code_review_graph/http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py) | Core middleware: `LoopbackOriginGuard`, `is_loopback_host()`, `split_host_port()`, `_authority_allowed()` |
| [`tests/test_http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/tests/test_http_origin_guard.py) | End-to-end security tests demonstrating DNS rebinding and cross-origin blocking |
| [`code_review_graph/__init__.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/__init__.py) | Package exports making the guard available to server entry points |
| [`code_review_graph/main.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/main.py) | CLI wiring—calls `build_http_middleware()` for `serve --http` |

## Summary

- **DNS rebinding attacks** exploit the mismatch between IP routing and hostname-based security by making malicious domains resolve to `127.0.0.1`
- The **HTTP transport origin guard** in [`code_review_graph/http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/http_origin_guard.py) blocks these attacks through strict `Host` and `Origin` header validation
- **Loopback detection** (`is_loopback_host`) ensures the guard only runs when appropriate—never interfering with intentional public exposure
- **Pure ASGI implementation** avoids buffering problems with streaming responses, making it production-ready for MCP servers
- Comprehensive tests in [`tests/test_http_origin_guard.py`](https://github.com/tirth8205/code-review-graph/blob/main/tests/test_http_origin_guard.py) verify protection against real-world attack scenarios

## Frequently Asked Questions

### Can the HTTP transport origin guard be disabled?

Yes. The guard disables itself automatically when the server binds to a non-loopback address like `0.0.0.0`. This detects operator intent: if you are deliberately exposing the server, the guard steps aside. There is no manual override for loopback binds because that would defeat the security purpose.

### Does the guard affect non-browser MCP clients?

No. Non-browser clients typically send neither `Origin` headers nor unusual `Host` headers, so they pass through with minimal overhead. The guard's design targets browser-based attack vectors while preserving compatibility with standard MCP tooling.

### Why not just check the source IP address?

Source IP verification is unreliable in modern networking. Proxies, NAT, and containerized environments can obscure the true client IP. The guard validates **semantic origin claims** (what the client says it is) rather than **network topology** (where packets appear to come from), which is both more precise and more portable.

### How does the guard handle IPv6 addresses?

The `split_host_port()` helper correctly parses bracketed IPv6 notation like `[::1]:5555`. IPv6 loopback addresses (`::1`) pass `is_loopback_host()` validation through Python's `ipaddress` module, ensuring consistent security across address families.