HIGH rate limiting bypassfastapijwt tokens

Rate Limiting Bypass in Fastapi with Jwt Tokens

Rate Limiting Bypass in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Rate limiting in Fastapi is commonly implemented using middleware that tracks requests per identity. When JWT tokens are used for authentication, a frequent pattern is to derive the rate limit key from a claim such as sub or a custom user_id. This introduces a bypass vector when the identifier is not consistently enforced across paths, when tokens contain mutable or missing claims, or when unauthenticated endpoints are mistakenly subjected to identity-based limits.

Consider a Fastapi app that applies a strict per-user rate limit using a Redis counter keyed by sub from a verified access token:

from fastapi import Fastapi, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
from typing import Optional

app = Fastapi()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

SECRET = "super-secret-key"
def decode_token(token: str):
    return jwt.decode(token, SECRET, algorithms=["HS256"])

def get_current_user(token: str = Depends(oauth2_scheme)):
    payload = decode_token(token)
    user_id: Optional[str] = payload.get("sub")
    if user_id is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication")
    return {"user_id": user_id}

@app.get("/data")
def read_data(user: dict = Depends(get_current_user)):
    return {"msg": f"Hello user {user['user_id']}"}

If the rate limiter uses only sub, an attacker who can cause the endpoint to be called without a valid token (for example, by exploiting unauthenticated paths or weak CORS preflight handling that allows unauthorized requests to appear legitimate) may avoid the limit. Similarly, if tokens include a mutable username or email instead of a stable identifier, and the implementation accidentally falls back to that field when sub is missing, the effective key becomes variable and easier to exhaust or share across identities.

Another bypass scenario involves token substitution across authorization flows. If an application accepts both access tokens (JWT) and API keys, and applies different rate limit granularities (for example, per IP for API keys and per user for JWT), an attacker can switch between authentication methods to amplify requests. This is especially relevant when token introspection or validation is incomplete, allowing unsigned or malformed tokens to be accepted under a permissive policy.

Because JWTs are self-contained, developers may assume validation is sufficient and skip additional checks on claims like iss, aud, or token binding. Without strict validation, an attacker can reuse an issued token intended for another purpose or a different client, causing the rate limit to be attributed to the wrong identity or not applied at all.

In black-box scanning, middleBrick’s Rate Limiting check flags inconsistencies such as missing identifier normalization, unauthenticated paths in identity-based limits, and mismatched enforcement between authentication schemes. Findings include severity and remediation guidance mapped to OWASP API Top 10 categories to help prioritize fixes.

Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes

To close rate limiting bypasses with JWT tokens in Fastapi, enforce stable, immutable identifiers and validate all relevant token claims. Always derive the rate limit key from a verified sub claim, and reject tokens that lack it. Ensure the same rate limiting logic applies regardless of authentication method, and avoid mixing identity sources.

Here is a hardened example that decodes and validates a JWT, extracts a stable user identifier, and applies a per-user rate limit using a dependency that is consistently reused:

from fastapi import Fastapi, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from typing import Optional
from starlette.responses import JSONResponse

app = Fastapi()
security = HTTPBearer()

SECRET = "super-secret-key"
ALGORITHM = "HS256"
RATE_LIMIT_WINDOW = 60  # seconds
RATE_LIMIT_MAX = 100    # requests per window

# In-memory store for example; use Redis in production
request_counts = {}

def verify_and_get_sub(token: str) -> str:
    try:
        payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM], audience="my-api", issuer="my-issuer")
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
    sub: Optional[str] = payload.get("sub")
    if not sub:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token missing sub claim")
    return sub

def rate_limit_middleware(request: Request, call_next):
    auth_header = request.headers.get("Authorization")
    if auth_header and auth_header.startswith("Bearer "):
        token = auth_header.split(" ")[1]
        user_id = verify_and_get_sub(token)
        key = f"rl:{user_id}"
    else:
        # Optionally apply a looser limit for unauthenticated paths
        key = f"rl:anonymous:{request.client.host}"
    current = request_counts.get(key, 0)
    if current >= RATE_LIMIT_MAX:
        return JSONResponse(
            status_code=429,
            content={"detail": "Rate limit exceeded"}
        )
    request_counts[key] = current + 1
    # Simplified: in production use a time-based store with TTL
    response = call_next(request)
    return response

@app.get("/data")
def read_data(request: Request, call_next=Depends()):
    return rate_limit_middleware(request, call_next)

Key improvements:

  • Strict JWT validation with aud and iss to prevent token misuse across services.
  • Mandatory sub claim for a stable identity; reject tokens that omit it.
  • A single, consistent rate limiting key derivation strategy that does not mix authentication methods.
  • Explicit handling of unauthenticated paths with a separate, clearly scoped key (e.g., IP-based) to avoid conflating limits.

In continuous monitoring, middleBrick’s Pro plan can track these controls over time and surface deviations in scoring. The dashboard and CLI can help verify that the same identifier is used consistently across routes and that no permissive fallbacks remain.

Remediation guidance from findings often references the need to align token validation with rate limiting logic, enforce claim requirements, and test behavior with invalid, missing, or substituted tokens. These steps reduce the likelihood of identity confusion and ensure that rate limits are predictable and enforceable.

Related CWEs: resourceConsumption

CWE IDNameSeverity
CWE-400Uncontrolled Resource Consumption HIGH
CWE-770Allocation of Resources Without Limits MEDIUM
CWE-799Improper Control of Interaction Frequency MEDIUM
CWE-835Infinite Loop HIGH
CWE-1050Excessive Platform Resource Consumption MEDIUM

Frequently Asked Questions

Why does using JWT sub alone not guarantee reliable rate limiting in Fastapi?
Using only the JWT sub claim can be bypassed if unauthenticated paths exist, if tokens lack sub, or if the application falls back to mutable fields (like email). Identifiers must be validated, required, and used consistently across all endpoints and authentication schemes to ensure accurate rate limiting.
How can I test that my rate limiting enforcement is not bypassable via token substitution in Fastapi?
Send requests with valid JWTs, unsigned tokens, and missing/modified claims to the same endpoint, ensuring each path enforces the same identifier derivation and that unauthenticated routes do not inadvertently share the same rate limit key. Automated scans can help detect inconsistencies between spec-defined authentication and runtime enforcement.