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 withHost: evil.example(classic DNS rebinding payload) receives 403 Forbiddentest_foreign_origin_is_rejected— Cross-origin browser requests withOrigin: http://evil.exampleare blockedtest_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.pyblocks these attacks through strictHostandOriginheader 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.pyverify 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →