HIGH race conditionfastapibasic auth

Race Condition in Fastapi with Basic Auth

Race Condition in Fastapi with Basic Auth — how this specific combination creates or exposes the vulnerability

A race condition in Fastapi with HTTP Basic Auth typically occurs when authorization state is checked and then acted upon without an atomic verify-and-execute pattern. Because Basic Auth sends credentials on each request, the framework can validate the username and password on every call; however, if your application logic performs a read-modify-write sequence across multiple steps or threads, an attacker can interleave requests to change state between checks.

Consider an endpoint that exposes a sensitive operation such as changing an email or initiating a password reset. The route first verifies the user’s credentials (e.g., via a dependency that decodes the Authorization header), then reads the current user record from a shared data store, and finally updates it. Between the read and the write, another authenticated request from the same or different session can mutate the user record (for example, changing roles or revoking permissions). This time-of-check-to-time-of-use (TOCTOU) pattern is the classic race condition, and it is amplified when authorization decisions are not tied directly to the request context.

Fastapi’s async nature can increase exposure if shared resources (caches, databases, files) are accessed concurrently without proper isolation or locking. An attacker may issue rapid, parallel requests with manipulated session identifiers or manipulated tokens to observe inconsistent behavior—such as acting as another user, exceeding rate limits, or triggering privileged actions without the required rights. Because Basic Auth does not inherently bind authorization to a cryptographic session, each request is evaluated independently, which makes it essential to design checks that are as close to the operation as possible.

In practice, this means you must treat the combination of Fastapi routes and HTTP Basic Auth as a trust boundary that requires strict, per-request validation and atomic decision-making. Do not rely on in-memory state or global variables to track permissions across multiple calls. Instead, enforce authorization at the point of data access and ensure that any change to critical resources is performed within a transaction or with optimistic concurrency controls that detect conflicting updates.

Basic Auth-Specific Remediation in Fastapi — concrete code fixes

To mitigate race conditions while using HTTP Basic Auth in Fastapi, couple strict per-request credential validation with atomic authorization checks and safe concurrency control. Below are concrete, working examples.

Secure Basic Auth dependency with per-request validation

Define a dependency that decodes and verifies credentials on every call, and that fetches the latest user data immediately before the route executes. This reduces the window where state can change between checks.

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import secrets

app = FastAPI()
security = HTTPBasic()

# Simulated user store; in production, use a transactional database.
USERS = {
    "alice": {"password": "s3cr3t", "role": "user", "email": "[email protected]"},
    "admin": {"password": "adm1n", "role": "admin", "email": "[email protected]"},
}

def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    user = USERS.get(credentials.username)
    if user is None or not secrets.compare_digest(user["password"], credentials.password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid credentials",
            headers={"WWW-Authenticate": "Basic"},
        )
    # Always return a fresh snapshot of the user data to avoid stale state.
    return {"username": credentials.username, "role": user["role"], "email": user["email"]}

Atomic authorization check inside the route

Perform authorization as the last step before the side-effect, and ensure any state changes are executed atomically. For a role-based change, validate the required role within the same function that performs the update.

@app.post("/promote")
def promote_user(target: str, current_user: dict = Depends(get_current_user)):
    if current_user["role"] != "admin":
        raise HTTPException(status_code=403, detail="Admin access required")
    # In a real app, use a database transaction to read and update in one atomic operation.
    if target not in USERS:
        raise HTTPException(status_code=404, detail="Target user not found")
    # Example of an unsafe pattern to avoid: reading user, then modifying elsewhere.
    # Instead, apply the change directly within this protected block.
    USERS[target]["role"] = "admin"
    return {"message": f"{target} promoted by {current_user['username']}"}

Concurrency-safe updates with versioning or locking

To prevent interleaved requests from overwriting each other, include a version or use database-level optimistic locking. Below is an illustrative pattern using a version number; adapt it to your actual persistence layer.

USERS_WITH_VERSION = {
    "alice": {"password": "s3cr3t", "role": "user", "email": "[email protected]", "_v": 1},
}

@app.post("/update-email")
def update_email(
    new_email: str,
    expected_version: int,
    current_user: dict = Depends(get_current_user)
):
    stored = USERS_WITH_VERSION.get(current_user["username"])
    if not stored:
        raise HTTPException(status_code=404, detail="User not found")
    if stored["_v"] != expected_version:
        raise HTTPException(status_code=409, detail="Resource changed, please retry")
    # Atomic update: validate and apply in one step relevant to your data layer.
    stored["email"] = new_email
    stored["_v"] += 1
    return {"email": stored["email"], "version": stored["_v"]}

Additional operational guidance

  • Always use HTTPS to protect credentials in transit; Basic Auth sends base64-encoded values that are easily decoded without encryption.
  • Prefer tokens or session-based auth for stateful interactions; if you must use Basic Auth, keep it strictly per-request and avoid storing authorization decisions between calls.
  • Log failed authentication attempts and monitor for rapid, parallel requests that may indicate probing for race conditions.

Frequently Asked Questions

Can a race condition still occur if I use HTTPS with Basic Auth in Fastapi?
Yes. HTTPS protects credentials in transit, but it does not prevent logical race conditions in your application code. A race condition is about timing and state changes between authorization checks and actions; HTTPS does not make per-request authorization atomic.
How can I test whether my Fastapi endpoints are vulnerable to race conditions when using Basic Auth?
Send rapid, parallel authenticated requests that read and then mutate shared state (for example, change roles or email). Observe whether inter-leaved requests allow an action to proceed with an outdated permission or identity. Automated security scans that include concurrency checks can help surface these patterns.