Prototype Pollution in Fastapi with Jwt Tokens
Prototype Pollution in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Prototype pollution in FastAPI applications that use JWT tokens can arise when user-controlled input modifies JavaScript object prototypes before the token is validated or while claims are processed. In Python, this often maps to manipulation of dictionaries or objects that later affect behavior, for example when nested data from request payloads or query parameters is merged into configuration or authorization structures without deep copying or strict validation. If a FastAPI endpoint accepts JSON that includes keys intended for object prototypes (e.g., __proto__, constructor, or custom properties) and merges them into a shared object used during JWT claim handling, it can change default behavior for token validation, such as altering expected claim checks or bypassing intended constraints.
When JWT tokens are involved, the risk surface includes the decoding and verification step. Although PyJWT and similar libraries parse and verify signatures server-side, applications may still process the decoded payload in unsafe ways. For instance, if the application takes claims from the token and merges them with request body data—perhaps to enrich a user context or build an access control decision—and that merge is performed on a shared mutable object, prototype-style keys can overwrite critical methods or attributes. This could change type checks, cause unexpected truthiness, or affect serialization, indirectly weakening authorization decisions. Moreover, if token introspection or revocation logic relies on object-like structures that are polluted, an attacker might influence whether a token is treated as valid or revoked.
Another angle specific to FastAPI is dependency injection and background tasks. If request state is attached to objects that are reused across requests and later consulted during JWT validation (for example, to determine scopes or tenant context), prototype pollution can inject properties that make a token appear to have elevated permissions or to belong to a different user. Even when FastAPI itself does not use prototypes directly, the ecosystem—such as libraries that manipulate class instances or Pydantic models in non-standard ways—can expose this issue. Therefore, securing the combination means validating and sanitizing all incoming data before it interacts with token handling logic and avoiding shared mutable structures for request-scoped authorization state.
Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes
To mitigate prototype pollution when JWT tokens are used in FastAPI, apply strict input validation, isolate token processing from mutable request data, and enforce least privilege for token usage. Below are concrete, safe patterns and code examples.
- Validate and sanitize all incoming data before merging with authorization logic. Use Pydantic models to define expected shapes and avoid generic dict merging that can be influenced by prototype-style keys.
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel, validator
import jwt
from typing import Optional
app = FastAPI()
class TokenPayload(BaseModel):
sub: str
scope: str
exp: int
@validator('scope')
def scope_must_be_allowed(cls, v):
allowed = {"read", "write", "admin"}
if v not in allowed:
raise ValueError(f"scope must be one of {allowed}")
return v
def decode_token(token: str, secret: str) -> TokenPayload:
try:
raw = jwt.decode(token, secret, algorithms=["HS256"])
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")
return TokenPayload(**raw)
@app.get("/protected")
async def read_protected(token: str = Depends(lambda req: req.query_params.get("token"))):
if not token:
raise HTTPException(status_code=400, detail="Missing token")
payload = decode_token(token, "super-secret-key")
# Use payload fields directly; do not merge with untrusted data
return {"user": payload.sub, "scope": payload.scope}
- Avoid merging token claims with mutable or shared objects. Instead, create isolated structures for authorization decisions and do not rely on object prototypes or dynamic property injection.
from fastapi import FastAPI, Depends
import jwt
app = FastAPI()
def get_token_scopes(token: str) -> set:
payload = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
scopes = payload.get("scope", "")
return set(s.strip() for s in scopes.split(",") if s.strip())
@app.get("/check")
async def check_scope(token: str = Depends(lambda req: req.query_params.get("token"))):
if not token:
raise HTTPException(status_code=400, detail="Missing token")
scopes = get_token_scopes(token)
# Do not mutate shared/global objects with token-derived data
required = {"read"}
if not required.issubset(scopes):
raise HTTPException(status_code=403, detail="Insufficient scope")
return {"ok": True}
- Use immutable data structures for configuration and authorization context. When integrating with libraries or background tasks, pass copies or frozen representations rather than references that could be altered through prototype-style keys.
import copy
from fastapi import BackgroundTasks
def safe_background_task(user_data: dict):
# Work on a deep copy to avoid accidental mutation of the original
safe_copy = copy.deepcopy(user_data)
# Process safe_copy without affecting shared state
...
@app.post("/enqueue")
async def enqueue_task(data: dict, background_tasks: BackgroundTasks):
# Ensure incoming data does not contain prototype-influenced keys before copying
cleaned = {k: v for k, v in data.items() if k not in ("__proto__", "constructor", "prototype")}
background_tasks.add_task(safe_background_task, cleaned)
return {"accepted": True}