Password Spraying in Fastapi with Api Keys
Password Spraying in Fastapi with Api Keys — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication attack technique where a single password is tried against many accounts. When an API uses static API keys to identify the application or service, but still relies on username/password endpoints for user login, the presence of the API key does not prevent password spraying against the user authentication path. In FastAPI, if routes like /login or token endpoints do not enforce strict rate limits or per-account attempt throttling, an attacker can iterate through common passwords across multiple usernames while presenting a valid API key to access public or authenticated endpoints that expose login feedback or account enumeration hints.
An API key in FastAPI is commonly passed in headers (e.g., X-API-Key) and used to allow access to certain endpoints without user credentials. However, if the application mixes API key identification with user-level password authentication, the API key does not protect the password verification logic. For example, an endpoint may validate the API key to permit request processing but then proceed to check username and password with a database query that leaks existence via timing differences or distinct response messages. This creates a combined risk: the API key identifies the client application, but the password spraying targets user accounts within that application context.
Consider an endpoint that accepts both an API key and a JSON body with username and password. If the endpoint returns different HTTP status codes or messages for "user not found" versus "invalid password," an attacker can enumerate valid usernames even when rate limiting is applied globally rather than per user. With API keys often shared among services or CI/CD systems, leaked keys further expand the attacker’s reach, enabling more aggressive spraying campaigns across distributed components. The FastAPI application might use HTTPBasic or OAuth2 password flows; if these are not carefully configured to enforce rate limits on a per-username or per-IP basis, password spraying becomes feasible.
Real-world attack patterns include using common passwords like Password123 or Welcome1 across thousands of usernames, monitoring response times and status codes to infer valid accounts. OWASP API Security Top 10 categories such as Broken Object Level Authorization (BOLA) and Rate Limiting weaknesses often intersect in these scenarios. Because API keys are static secrets, they must be treated like any other credential: rotation, restricted scopes, and binding to specific endpoints are essential. Without per-user rate limiting or secure password hashing with constant-time comparison, even robust hashing in the database cannot prevent an attacker from iterating guesses quickly through the API.
middleBrick detects scenarios where authentication endpoints coexist with API key usage and identifies risk patterns such as missing per-account throttling or inconsistent error handling. In scans that include LLM/AI Security checks, it also looks for potential prompt injection attempts that could manipulate an API if authentication logic is exposed via model inputs. Findings include severity-ranked guidance to tighten authentication logic, align with OWASP API Top 10, and verify compliance with frameworks like PCI-DSS and SOC2.
Api Keys-Specific Remediation in Fastapi — concrete code fixes
To remediate password spraying when using API keys in FastAPI, enforce per-user rate limiting, ensure consistent authentication response behavior, and tightly bind API keys to intended usage. Below are concrete code examples that demonstrate secure patterns.
First, use a per-user rate limiter rather than only global limits. With fastapi.middleware.RateLimiter patterns or third-party libraries, track attempts by username and apply stricter caps for suspicious activity. Here is an example using a simple in-memory store for illustration; production systems should use Redis or similar shared stores in distributed deployments:
from fastapi import FastAPI, Depends, HTTPException, Header, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import time
app = FastAPI()
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
# In-memory store for per-user attempt tracking (use Redis in production)
user_attempts = {}
def get_username_from_api_key(api_key: str) -> str | None:
# Map API key to owning user or service; in practice use a secure lookup
mapping = {
"s3cr3tK3y_abc": "service_account",
"s3cr3tK3y_xyz": "admin_service",
}
return mapping.get(api_key)
@app.post("/login")
@limiter.limit("5/minute")
async def login(
credentials: dict = Depends(),
api_key: str = Header(None)
):
username = credentials.get("username")
password = credentials.get("password")
if not username or not password:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing credentials")
# Use constant-time comparison where possible; here simplified for clarity
user = get_username_from_api_key(api_key)
if user is None:
# Do not reveal key validity through timing; simulate work
time.sleep(0.1)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
# Track per-username attempts to prevent spraying
key = f"{user}:{username}"
now = time.time()
attempts = user_attempts.get(key, [])
attempts = [t for t in attempts if now - t < 60]
if len(attempts) >= 10:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many attempts")
user_attempts.setdefault(key, []).append(now)
# Simulated user verification; in real apps, use hashed password checks
if username == "alice" and password == "StrongPass!23":
return {"token": "fake-jwt-token"}
else:
# Return the same status and timing regardless of username existence
time.sleep(0.2)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
Second, ensure that error responses do not disclose whether an API key or username is valid. Return uniform messages and status codes, and introduce small, consistent delays to mitigate timing attacks. Also rotate API keys regularly and scope them to least privilege via environment variables or secure vaults, not hardcoded strings.
For integration into CI/CD, the middleBrick GitHub Action can be added to fail builds if risk scores exceed your threshold, while the CLI allows local scanning with middlebrick scan <url>. If you use the Dashboard, you can track how remediation changes affect your security scores over time.