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:
- The host resolves to a loopback address
- 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
Originheaders (https://evil.com) - Rebinding
Hostheaders (malicious.local) - Wrong ports (
127.0.0.1:8080when server is on5555) - 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:
- Attacker sets up
evil.comwith DNS TTL of 0 seconds - Victim visits
evil.com→ resolves to attacker's server (establishes same-origin) - DNS record flips to
127.0.0.1 - Malicious JavaScript sends
fetch("http://127.0.0.1:5555/mcp/") - Browser includes
Origin: https://evil.com(cross-origin) LoopbackOriginGuarddetectsOriginheader points toevil.com- 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
LoopbackOriginGuardmiddleware only enforces checks when bound to loopback addresses, avoiding disruption of intentional external exposure. -
Dual-header validation: Both
HostandOriginheaders 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_middlewarehelper andmain.pywiring 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →