Password Spraying in Fastapi
How Password Spraying Manifests in FastAPI
Password spraying is a credential-stuffing variant where attackers try a few common passwords (e.g., Password123, admin) across many user accounts to avoid account lockouts. In FastAPI applications, this attack exploits two common misconfigurations: missing rate limiting on authentication endpoints and inefficient credential verification that doesn't amplify attack cost.
FastAPI's dependency injection system often centralizes authentication logic. A typical vulnerable pattern uses a custom Depends() for login that validates credentials against a database but lacks throttling:
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
def get_db():
# Database session generator
...
def authenticate_user(username: str, password: str, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == username).first()
if not user or not verify_password(password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
return user
@app.post("/login")
async def login(user: UserCreate, db: Session = Depends(get_db)):
return authenticate_user(user.username, user.password, db)
Here, each authentication attempt—successful or not—triggers a database query and password hash verification (e.g., via passlib). An attacker can script thousands of attempts per minute from a single IP because FastAPI, by default, has no built-in rate limiting. This aligns with OWASP API Security Top 10 2023: API2:2023 – Broken Authentication and CWE-307: Improper Restriction of Excessive Authentication Attempts.
Another vector: when using JWT, some implementations grant long-lived tokens upon initial authentication without re-verifying credentials on subsequent sensitive operations. If an attacker sprays a password and obtains a token, they may have extended access. FastAPI's OAuth2PasswordRequestForm doesn't enforce any usage limits by itself.
FastAPI-Specific Detection
Detecting password spraying vulnerabilities involves testing for the absence of rate limiting and inefficient credential checks. As a black-box scanner, middleBrick probes authentication endpoints (like /login, /auth/token) with rapid, repeated requests using common passwords to observe response patterns and timing. It checks for:
- Uniform response times: If every failed attempt takes the same time (e.g., 200ms), it suggests full password hashing occurs each time, enabling efficient spraying.
- No
429 Too Many Requestsheaders: Absence indicates no rate limiting. - Account enumeration via response differences: Distinct messages for "user not found" vs. "invalid password" (e.g.,
{"error":"User does not exist"}vs.{"error":"Invalid password"}) let attackers map valid usernames first.
You can manually replicate this with curl and a wordlist, but middleBrick's CLI automates it. Install and run:
npm install -g middlebrick
middlebrick scan https://your-fastapi-app.com/loginThe scan returns an Authentication risk score (part of the A–F grade) and specific findings like "No rate limiting detected on authentication endpoint" with remediation steps. The Pro plan's continuous monitoring can track if rate limiting regresses after deployment.
FastAPI-Specific Remediation
Remediation requires implementing rate limiting and consistent error responses. Use slowapi (a Starlette/FastAPI rate limiter) or fastapi-limiter. Here's a secure pattern with slowapi and Redis for distributed limits:
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import FastAPI, Request, Depends, HTTPException
from redis import Redis
redis_client = Redis(host="localhost", port=6379, db=0)
limiter = Limiter(key_func=get_remote_address, storage_uri="redis://localhost:6379")
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
def verify_credentials(username: str, password: str):
# Constant-time check to prevent timing attacks
user = get_user(username) # Fetch user if exists
if not user:
# Hash a dummy password to maintain consistent timing
verify_password("dummy", "dummy_hashed")
return False
return verify_password(password, user.hashed_password)
@app.post("/login")
@limiter.limit("5/minute") # 5 attempts per minute per IP
async def login(request: Request, username: str, password: str):
if not verify_credentials(username, password):
# Generic message—no user enumeration
raise HTTPException(status_code=401, detail="Invalid credentials")
# Issue JWT token
return {"access_token": create_token(username)}
Key fixes:
- Rate limiting:
@limiter.limit("5/minute")throttles per IP. Adjust limits based on risk (e.g., lower for admin endpoints). Use"10/5 minutes"for more flexibility. - Constant-time verification: Always hash a dummy password if user not found to prevent timing side-channels.
- Generic error messages: Never reveal if username exists.
- Distributed storage: Redis ensures limits work across multiple FastAPI workers (crucial for production).
For JWT-based APIs, also limit token refresh endpoints and consider short-lived tokens with refresh rotation. The middleBrick GitHub Action can enforce that these controls are present before merge—configure it to fail PRs if the Authentication score drops below a threshold (e.g., B).
Understanding the Scoring
middleBrick's Authentication category (part of the overall 0–100 score) directly reflects password spraying risk. It evaluates:
- Presence and configuration of rate limits (e.g.,
429responses,Retry-Afterheaders). - Consistency of error messages (no user enumeration).
- Credential verification efficiency (constant-time compares).
- MFA enforcement for sensitive operations.
A score of 90+ (A) requires robust, distributed rate limiting and uniform errors. A 50–69 (D) might show rate limits only on success paths or missing limits entirely—classic password spraying enablers. The Starter plan's email alerts notify you if this score degrades.
Frequently Asked Questions
Is password spraying only a concern for username/password login endpoints?
HTTPBasic or custom token schemes are equally vulnerable if rate limiting is absent. middleBrick scans all authentication surfaces.