Timing Attack in Fastapi with Basic Auth
Timing Attack in Fastapi with Basic Auth — how this specific combination creates or exposes the vulnerability
A timing attack in Fastapi with HTTP Basic Auth arises because the framework typically compares credentials using a linear string comparison. When a server compares the provided password with the stored hash or secret character by character, the comparison can short-circuit on the first mismatching byte. This introduces measurable differences in response time that an attacker can detect by sending many requests and observing small variations in latency. These timing differences can leak information about how many initial characters of the password or hash match, enabling an attacker to gradually infer the correct credential without ever triggering a failed-login rate limit that might otherwise block brute-force attempts.
Fastapi itself does not prescribe a particular authentication implementation; the vulnerability depends on how the developer wires up Basic Auth. If the developer directly compares the raw password strings or compares hashes with a standard equality operator, the comparison time becomes data. Even when using secure hash functions like SHA-256 or bcrypt, if the comparison of the derived key is not constant-time, the password verification step remains a bottleneck an attacker can measure. The attack surface is unauthenticated because middleBrick tests the endpoint in its default state; if Basic Auth is missing or misconfigured, the timing discrepancy can be observed more easily, and the server may reveal subtle differences between a missing Authorization header and a malformed one.
Consider a Fastapi route that manually checks username and password like this, which is vulnerable:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import secrets
app = FastAPI()
security = HTTPBasic()
# Insecure: plain comparison
USERS = {"admin": "S3cretP@ssw0rd"} # plaintext example only
def get_user(credentials: HTTPBasicCredentials = Depends(security)):
expected = USERS.get(credentials.username)
if not expected or credentials.password != expected: # vulnerable to timing
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/secure")
def read_secure(username: str = Depends(get_user)):
return {"msg": f"Hello, {username}"}
An attacker can send requests with a guessed first character and measure response times; a slightly longer delay may indicate a correct first character because the comparison proceeds to the next byte. By repeating this process sequentially, the attacker can infer the password character by character. Even when using hashed storage, if the verification routine does not use a constant-time comparison, the hash comparison becomes the weak link. middleBrick’s 12 checks run in parallel and include checks for Authentication and Input Validation, surfacing weak credential handling patterns that may enable timing-based inference.
Basic Auth-Specific Remediation in Fastapi — concrete code fixes
Remediation focuses on ensuring that password or hash verification does not branch on secret-dependent conditions. The primary fix is to replace standard equality checks with a constant-time comparison routine. Additionally, ensure that the server’s response for missing credentials and for incorrect credentials is consistent in both status code and body, to remove timing side channels that distinguish one failure mode from another.
Use secrets.compare_digest for Python strings, which performs a constant-time comparison suitable for mitigating timing attacks. Combine this with uniform error responses and proper HTTP status codes so that an attacker cannot distinguish between a nonexistent user and a wrong password based on timing or response content alone.
Here is a secure Fastapi implementation using HTTP Basic Auth:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import secrets
app = FastAPI()
security = HTTPBasic()
# Secure storage: in practice store bcrypt/argon2 hashes
USERS = {"admin": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"} # sha256 hash of "password"
def verify_password(plain_password: str, hashed_password: str) -> bool:
# If you store raw passwords (not recommended), use constant-time compare
return secrets.compare_digest(plain_password, hashed_password)
def get_user(credentials: HTTPBasicCredentials = Depends(security)):
expected_hash = USERS.get(credentials.username)
if expected_hash is None:
# Still perform a dummy comparison to keep timing similar
secrets.compare_digest("dummy", "dummy")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
if not verify_password(credentials.password, expected_hash):
# Constant-time failure path
secrets.compare_digest("dummy", "dummy")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/secure")
def read_secure(username: str = Depends(get_user)):
return {"msg": f"Hello, {username}"}
For production, store salted, slow hashes (e.g., bcrypt or Argon2) and compare the derived hash using a constant-time routine. The above pattern ensures that both missing users and wrong passwords follow the same code path and take approximately the same time, reducing the usefulness of timing-based inference. middleBrick’s scans can validate that your authentication endpoints do not exhibit detectable timing variance patterns and that remediation aligns with secure coding practices.