Credential Stuffing in Fastapi (Python)
Credential Stuffing in Fastapi with Python — how this specific combination creates or exposes the vulnerability
Credential stuffing attacks exploit the reuse of credentials across services, and FastAPI applications built with Python are particularly vulnerable when they implement authentication endpoints that accept credentials without rate limiting or additional safeguards. When a FastAPI endpoint is exposed and processes requests using standard HTTP methods like POST without throttling, attackers can automate large volumes of login attempts using leaked username and password pairs from public data breaches. Because FastAPI is asynchronous and designed for high concurrency, it can handle these bursts of requests efficiently, which means a poorly configured endpoint may process thousands of credential pairs per minute, overwhelming the system and potentially gaining unauthorized access to user accounts.
In a typical FastAPI setup using Python's fastapi and uvicorn, an endpoint might look like this:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
import hashlib
import os
app = FastAPI()
# Simplified credential store (hashed passwords)
USER_DB = {
"admin": hashlib.sha256("admin_pass").hexdigest(),
"user1": hashlib.sha256("user1_pass").hexdigest(),
}
class LoginRequest(BaseModel):
username: str
password: str
@app.post("/login")
async def login(request: LoginRequest):
hashed = hashlib.sha256(request.password.encode()).hexdigest()
if USER_DB.get(request.username) == hashed:
return {"status": "success", "message": "Login granted"}
else:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")This endpoint has no rate limiting, no account lockout mechanism, and no CAPTCHA or bot detection, making it susceptible to credential stuffing. Attackers can script hundreds of requests using tools like curl or Python's requests library to test large credential lists against the /login endpoint. Because FastAPI runs on an event loop, it processes these requests concurrently, which amplifies the attack speed. Without protective measures, an attacker can exhaust user accounts, harvest successful logins, or trigger denial-of-service conditions.
Additionally, FastAPI applications often integrate with third-party identity providers or use JWT tokens for session management. If these tokens are short-lived and not validated properly, or if session identifiers are predictable, attackers may reuse compromised credentials across related API endpoints. The combination of Python's ecosystem of lightweight web frameworks and FastAPI's performance model makes it essential to harden authentication endpoints specifically against automated credential injection, not just general brute force attacks.
Python-Specific Remediation in Fastapi — concrete code fixes
To mitigate credential stuffing in a FastAPI application, developers must introduce rate limiting, account lockout policies, and robust authentication checks that go beyond simple password verification. One effective approach is to integrate Python's slowapi library, which provides middleware for rate limiting based on IP address or endpoint. Additionally, using starlette-limiter or implementing custom logic with Redis-backed counters can prevent repeated login attempts from a single source. Below is a revised version of the earlier endpoint that includes rate limiting and account lockout logic:
from fastapi import FastAPI, HTTPException, status, Request
from pydantic import BaseModel
import hashlib
app = FastAPI()
# Simulated user database with hashed passwords
USER_DB = {
"admin": hashlib.sha256("admin_pass").hexdigest(),
"user1": hashlib.sha256("user1_pass").hexdigest(),
}
# Track failed attempts per IP
from collections import defaultdict
failed_attempts = defaultdict(int)
MAX_ATTEMPTS = 5
LOCKOUT_TIME = 300 # 5 minutes
class LoginRequest(BaseModel):
username: str
password: str
@app.post("/login")
async def login(request: Request, login_data: LoginRequest):
# Extract client IP
client_ip = request.client.host
# Rate limiting
failed_attempts[client_ip] += 1
if failed_attempts[client_ip] > MAX_ATTEMPTS:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many login attempts. Try again later."
)
hashed = hashlib.sha256(login_data.password.encode()).hexdigest()
if USER_DB.get(login_data.username) == hashed:
# Reset counter on successful login
failed_attempts[client_ip] = 0
return {"status": "success", "message": "Login granted"}
else:
# Still allow attempts but increment counter
failed_attempts[client_ip] += 1
# Optional: introduce exponential backoff
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials"
)For stronger protection, integrate a Redis-backed store to persist attempt counts across distributed deployments, and consider using libraries like fastapi-limiter that support Redis and in-memory stores. Additionally, enforce HTTPS, use CAPTCHA challenges for suspicious activity, and implement multi-factor authentication (MFA) where possible. These measures collectively reduce the risk of credential stuffing by slowing down automated attacks and detecting suspicious behavior patterns.