How the HTTP Transport Origin Guard Prevents DNS Rebinding Attacks

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 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. 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.


# 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:

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:

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:

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:

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 constructs the middleware stack:


# 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:

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 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 Core middleware: LoopbackOriginGuard, is_loopback_host(), split_host_port(), _authority_allowed()
tests/test_http_origin_guard.py End-to-end security tests demonstrating DNS rebinding and cross-origin blocking
code_review_graph/__init__.py Package exports making the guard available to server entry points
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 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 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.

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 →