Padding Oracle in Fastapi with Jwt Tokens
Padding Oracle in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A padding oracle arises when an application reveals whether decrypted data has valid padding, enabling an attacker to iteratively decrypt ciphertext without knowing the key. In FastAPI applications that accept JSON Web Tokens (JWT), this typically occurs when the server performs decryption and signature verification in a way that leaks padding validity through timing differences or error messages.
JWTs are often represented as three dot-separated base64url-encoded parts: header.payload.signature. When the payload is encrypted (JWE) rather than signed (JWS), the server must decrypt it before validating claims. If the cryptographic library returns distinct errors for invalid padding versus invalid signature, an attacker can supply modified ciphertexts and observe differences in HTTP response codes or response times. FastAPI routes that parse JWTs via libraries that do not use constant-time verification or that expose stack traces can inadvertently act as an oracle.
Consider a FastAPI endpoint that expects an encrypted JWT in an Authorization header:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
SECRET_KEY = "example-secret"
ALGORITHM = "HS256"
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except jwt.PyJWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
@app.get("/items/")
async def read_items(user: dict = Depends(get_current_user)):
return {"message": "secure data"}
If the jwt.decode implementation uses an algorithm that is vulnerable to padding manipulation (e.g., when keys are mismanaged and the token is expected to be encrypted rather than signed), and the library reveals padding errors distinctly, an attacker can perform adaptive chosen-ciphertext attacks. Even when using HMAC-SHA algorithms, improper handling of token validation order can expose behavior that aids an attacker who can distinguish between malformed tokens and valid-but-modified tokens.
In practice, a FastAPI application using JWTs may expose a padding oracle if:
- It returns different HTTP status codes or messages for decryption failures versus signature failures.
- It uses non-constant-time verification functions that early-exit on padding errors.
- Stack traces or debug details are returned in responses, making the oracle observable.
middleBrick scans this surface by testing unauthenticated endpoints that accept JWTs, checking for timing variance and informative error messages, and mapping findings to the Authentication and Input Validation checks. This helps surface whether the API unintentionally exposes behavior that could assist a padding oracle attack.
Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes
Remediation centers on ensuring that token validation fails in a uniform, opaque manner and that algorithms and libraries are configured to avoid padding-sensitive operations. Below are concrete, working FastAPI examples that address padding-related risks when using JWTs.
1) Use signed JWTs (JWS) with strong algorithms and avoid encrypted JWTs (JWE) unless you have a vetted library that uses constant-time decryption. Always explicitly specify algorithms and verify the signature before processing claims.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
SECRET_KEY = "your-256-bit-secret"
ALGORITHM = "HS256"
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
# Explicitly restrict algorithms to prevent algorithm confusion
payload = jwt.decode(
credentials.credentials,
SECRET_KEY,
algorithms=[ALGORITHM],
options={"verify_signature": True, "verify_exp": True},
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired",
headers={"WWW-Authenticate": "Bearer"},
)
except jwt.InvalidTokenError:
# Use a generic message and consistent HTTP status to avoid leaking validation details
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
@app.get("/items/")
async def read_items(token_info: dict = Depends(verify_token)):
return {"message": "secure data", "user": token_info}
2) Ensure your JWT library is up to date and avoid constructs that rely on encryption modes susceptible to padding oracles (e.g., certain CBC-based JWE). If you must handle encrypted tokens, prefer libraries that implement constant-time decryption and avoid returning distinct errors for padding failures.
# Example of requesting a signed compact JWT (JWS) instead of an encrypted JWT
# This token is generated with HS256 and contains claims as a JSON payload.
# client = httpx.Client()
# response = client.post("/token", json={"alg": "HS256", "typ": "JWT"}, data=payload)
3) Add consistent error handling and avoid exposing stack traces in production. Configure FastAPI to return generic responses for validation errors and disable debug exceptions in production.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI(debug=False)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
4) When possible, use asymmetric algorithms (e.g., RS256) and validate audience and issuer claims to further reduce risk. Always validate token structure before attempting decryption, and enforce short expiration times to limit the window for replay or adaptive attacks.
import jwt
from jwt import PyJWKClient
url = "https://example.com/.well-known/jwks.json"
jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="my-audience",
issuer="https://auth.example.com",
options={"verify_exp": True, "require": ["iss", "aud"]},
)
These patterns reduce the risk that token handling introduces a padding oracle by ensuring uniform failure behavior, limiting the information exposed during validation, and using well-vetted algorithms and libraries.