Dns Rebinding in Fastapi (Python)
Dns Rebinding in Fastapi with Python — how this specific combination creates or exposes the vulnerability
DNS rebinding is a client-side attack where an attacker tricks a victim’s browser into resolving a domain name to an IP address under the attacker’s control, then rapidly changing the resolved address to a different target, often a private or internal host. In a FastAPI application written in Python, this can expose internal services or administrative interfaces to external clients when the application relies solely on hostname-based access controls or trusts the Host header for routing decisions.
FastAPI, built on Starlette and Pydantic, does not inherently prevent DNS rebinding. If your Python code uses the request’s hostname to determine access permissions—such as allowing localhost or internal network IPs—malicious JavaScript running in a browser can make a request to your FastAPI endpoint, then force the connection to pivot to an internal service (e.g., 127.0.0.1:8000/admin) after the TCP connection is established. Because the TLS handshake and HTTP request may appear valid, server-side checks based on the original hostname can be bypassed.
Consider a FastAPI endpoint that only permits access if the request host is localhost:
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.get("/health")
async def health_check(request: Request):
if not request.url.hostname == "localhost":
raise HTTPException(status_code=403, detail="Forbidden")
return {"status": "ok"}
An attacker can register a domain (e.g., example.attacker.com) that initially resolves to a public IP they control, then after the TLS connection is established, switch the DNS response to point to 127.0.0.1. The victim’s browser maintains the TCP connection, and the FastAPI app sees a request that appears to come from localhost, bypassing the hostname check. This scenario is especially relevant when the FastAPI app is bound to 0.0.0.0 and exposed on a network where internal services are reachable.
DNS rebinding is not specific to FastAPI or Python, but Python-based services that assume network perimeter boundaries are trustworthy are at risk. The attack surface is larger when your API accepts unauthenticated requests and exposes endpoints that should only be accessed internally. middleBrick’s scans detect unauthenticated attack surfaces and can surface DNS rebinding-like exposure patterns when combined with improper host or IP-based authorization checks.
Python-Specific Remediation in Fastapi — concrete code fixes
To mitigate DNS rebinding in FastAPI Python applications, implement host-agnostic authorization and avoid trusting the request hostname for security decisions. Instead of checking request.url.hostname, use explicit allowlists of public-facing domains or enforce authentication and authorization at the resource level. Below are concrete, Python-specific fixes.
1. Use an explicit public hostname allowlist with exact matching
Define a constant set of allowed hostnames and validate against it rather than relying on inequality checks. This prevents attackers from dynamically changing the target IP after connection establishment.
from fastapi import FastAPI, Request, HTTPException
ALLOWED_HOSTS = {"api.example.com", "www.example.com"}
app = FastAPI()
@app.middleware("http")
async def verify_host(request: Request, call_next):
if request.url.hostname not in ALLOWED_HOSTS:
raise HTTPException(status_code=403, detail="Forbidden hostname")
response = await call_next(request)
return response
@app.get("/health")
async def health_check():
return {"status": "ok"}
2. Bind FastAPI explicitly to public interfaces and avoid relying on loopback assumptions
When starting your server, explicitly bind to the intended public interface instead of 0.0.0.0 unless necessary. In development, you can still use 0.0.0.0, but enforce strict host checks in production code as shown above.
# Run with: uvicorn main:app --host 0.0.0.0 --port 8000
# Then enforce hostname checks via middleware as shown earlier.
3. Validate and sanitize any user-supplied Host header usage
If your application logic must inspect the host, treat it as untrusted input. Use Pydantic validators or manual checks to ensure it conforms to expected patterns and does not resolve to private IP ranges.
import ipaddress
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
_PRIVATE_SUBNETS = ["10.0.0.0/8", "192.168.0.0/16", "127.0.0.0/8"]
def is_private_ip(hostname: str) -> bool:
try:
ip = ipaddress.ip_address(hostname)
return any(ip in ipaddress.ip_network(net) for net in _PRIVATE_SUBNETS)
except ValueError:
return False
@app.middleware("http")
async def prevent_private_access(request: Request, call_next):
hostname = request.url.hostname
if hostname and (is_private_ip(hostname) or hostname == "localhost"):
raise HTTPException(status_code=403, detail="Private host access denied")
response = await call_next(request)
return response
These Python patterns reduce the risk of DNS rebinding by eliminating implicit trust in network location or hostname headers. Combine them with authentication, rate limiting, and regular scans using tools like middleBrick to identify exposed endpoints and insecure routing logic. The CLI (middlebrick scan <url>) and GitHub Action can help you detect these issues early in development and CI/CD pipelines.