HIGH dictionary attackfastapijwt tokens

Dictionary Attack in Fastapi with Jwt Tokens

Dictionary Attack in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability

A dictionary attack against a FastAPI service that uses JWT tokens typically targets the authentication endpoint rather than the token validation itself. Attackers use a list of common usernames or emails combined with guessed or leaked passwords, making repeated POST requests to the login route. If the endpoint reveals whether a user exists (user enumeration) or does not enforce sufficient rate limiting, the attacker can identify valid accounts and then focus brute-force efforts on those accounts.

JWT tokens themselves are not the primary target in this scenario; instead, the token issuance path is abused. When tokens are issued without strict constraints on authentication attempts, an attacker can obtain a valid token for a discovered account. This is especially risky if tokens have long lifetimes, are transmitted over unencrypted channels, or if the token signing secret is weak or leaked. The FastAPI application may also inadvertently expose timing differences in password verification or token generation, which can be leveraged to infer valid users.

In an unauthenticated scan, middleBrick tests the authentication surface by probing for user enumeration, weak lockout mechanisms, and insufficient rate limiting. One of the 12 parallel security checks, Authentication, examines whether the API reveals account existence through different responses for existing versus non-existing users. Another related check, Rate Limiting, evaluates whether the endpoint throttles requests aggressively enough to deter rapid guessing. If the API accepts a high number of login attempts per minute, a dictionary attack becomes feasible without triggering defenses.

Because JWTs are often used for stateless session management, a compromised token can grant extended access across protected routes. middleBrick’s BOLA/IDOR and Property Authorization checks look for insecure direct object references or missing ownership validation on endpoints that rely on token claims. If authorization logic does not properly validate scopes or roles encoded in the JWT, an attacker who obtains a token may escalate privileges or access other users’ data. The scan also inspects whether tokens are transmitted over HTTPS-only channels and whether sensitive data is exposed in token payloads, addressing Data Exposure and Encryption checks.

To illustrate a typical vulnerable FastAPI login route, consider the following example where user enumeration is possible and rate limiting is absent:

from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.security import OAuth2PasswordRequestForm
from typing import Optional

app = FastAPI()

# In-memory store for demonstration only
USERS_DB = {
    "alice": {"username": "alice", "password": "CorrectHorseBatteryStaple", "role": "user"},
    "admin": {"username": "admin", "password": "SuperSecret123!", "role": "admin"},
}

@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = USERS_DB.get(form_data.username)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    if user["password"] != form_data.password:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )
    # Insecure: no rate limiting, no token binding, long-lived token possible
    return {"access_token": f"mock-jwt-for-{user['username']}", "token_type": "bearer"}

In this example, the response message is identical for both a non-existing user and a wrong password, but subtle timing differences or logging behavior may still hint at user existence. There is no limit on login attempts, and the token returned is a mock string rather than a securely signed JWT. An attacker can run a dictionary attack by cycling through common usernames and passwords, harvesting valid tokens for subsequent abuse. middleBrick’s scans would flag this as high risk under Authentication and Rate Limiting, and would recommend improvements such as consistent error responses, account lockout, and secure token handling.

Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes

Securing JWT-based authentication in FastAPI requires reducing information leakage, enforcing strict rate limits, and ensuring tokens are issued and validated safely. Below are concrete, actionable code changes that address dictionary attack risks while maintaining compatibility with standard JWT workflows.

First, use a constant-time comparison for credentials and return a generic error message to prevent user enumeration:

from fastapi import FastAPI, HTTPException, status, Depends
from fastapi.security import OAuth2PasswordRequestForm
from typing import Optional
import secrets

app = FastAPI()

USERS_DB = {
    "alice": {"username": "alice", "password_hash": "$argon2id$v=19$m=65536,t=3,p=4$...", "role": "user"},
    "admin": {"username": "admin", "password_hash": "$argon2id$v=19$m=65536,t=3,p=4$...", "role": "admin"},
}

def safe_login_response():
    # Always return the same status and generic message
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Incorrect username or password",
        headers={"WWW-Authenticate": "Bearer"},
    )

@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = USERS_DB.get(form_data.username)
    # Use secrets.compare_digest to mitigate timing attacks
    if user is None or not secrets.compare_digest(user["password_hash"], hash_password(form_data.password)):
        return safe_login_response()
    # Issue a properly signed JWT in production (example uses a mock)
    access_token = create_jwt_token(subject=user["username"], role=user["role"])
    return {"access_token": access_token, "token_type": "bearer"}

Second, enforce rate limiting at the authentication endpoint to deter rapid guessing. Using a simple in-memory counter for illustration, you can integrate a more robust solution such as Redis in production:

from fastapi import FastAPI, HTTPException, status, Depends, Request
from fastapi.security import OAuth2PasswordRequestForm
from datetime import datetime, timedelta
from typing import Dict

app = FastAPI()
login_attempts: Dict[str, int] = {}
LOCKOUT_THRESHOLD = 5
LOCKOUT_WINDOW = 60  # seconds

def is_rate_limited(identifier: str) -> bool:
    attempts = login_attempts.get(identifier, [])
    # Clean old attempts
    recent = [t for t in attempts if datetime.utcnow() - t < timedelta(seconds=LOCKOUT_WINDOW)]
    login_attempts[identifier] = recent
    return len(recent) >= LOCKOUT_THRESHOLD

def record_attempt(identifier: str):
    login_attempts.setdefault(identifier, []).append(datetime.utcnow())

@app.post("/login")
def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
    client_ip = request.client.host
    if is_rate_limited(client_ip):
        raise HTTPException(status_code=429, detail="Too many attempts. Try again later.")
    user = USERS_DB.get(form_data.username)
    if user is None or not secrets.compare_digest(user["password_hash"], hash_password(form_data.password)):
        record_attempt(client_ip)
        return safe_login_response()
    record_attempt(client_ip)
    access_token = create_jwt_token(subject=user["username"], role=user["role"])
    return {"access_token": access_token, "token_type": "bearer"}

Third, ensure JWTs are issued with appropriate claims, short lifetimes, and signed using a strong algorithm. Here is a secure example using PyJWT:

import jwt
from datetime import datetime, timedelta

SECRET_KEY = "your-very-strong-secret"
ALGORITHM = "HS256"

def create_jwt_token(subject: str, role: str) -> str:
    to_encode = {"sub": subject, "role": role}
    expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

Validate tokens on protected endpoints and check scopes or roles as needed. Also enforce HTTPS in production to protect tokens in transit, and avoid including sensitive data in the JWT payload. These changes reduce the effectiveness of dictionary attacks by limiting rapid attempts, avoiding user enumeration, and ensuring tokens are short-lived and securely handled.

Frequently Asked Questions

Can a dictionary attack succeed if the API uses JWT tokens but lacks rate limiting?
Yes. Without rate limiting on the login endpoint, attackers can make many password guesses per minute, increasing the likelihood of discovering valid credentials and obtaining JWT tokens.
Does using JWT tokens prevent user enumeration if error messages differ between invalid users and wrong passwords?
No. If responses or timing differ between non-existing users and incorrect passwords, attackers can still enumerate accounts even when JWT tokens are used for session management.