Mass Assignment in Fastapi with Api Keys
Mass Assignment in Fastapi with Api Keys — how this specific combination creates or exposes the vulnerability
Mass assignment occurs when an API binds user-supplied data directly to a model or function parameters without explicit allowlisting of fields. In FastAPI, this commonly happens when a Pydantic model is used with Field(..., include) omitted or when a generic dictionary is passed to business logic. When API keys are used for authentication but authorization checks are incomplete, an attacker who obtains or guesses a valid key can exploit mass assignment to modify sensitive attributes they should not control.
Consider a FastAPI endpoint that accepts a payload to update a user profile and uses an API key via an HTTPBearer scheme for authentication. If the endpoint maps incoming JSON directly to a SQLAlchemy model or a Pydantic model with unchecked fields, an attacker can add or overwrite fields such as is_admin, role, or permissions. Because the request includes a valid API key, the request passes authentication, but the application incorrectly assumes the caller is authorized to set any field. This specific combination—authentication via API keys coupled with mass assignment—creates a privilege escalation path that is hard to detect without explicit field controls.
Insecure endpoint example:
from fastapi import FastAPI, Depends, HTTPException, Header
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# Simulated API key store
VALID_API_KEYS = {"secret-key-123": "user:alice"}
class UserUpdate(BaseModel):
username: Optional[str] = None
email: Optional[str] = None
is_admin: Optional[bool] = None # dangerous if not filtered
def get_current_user(x_api_key: str = Header(...)):
user = VALID_API_KEYS.get(x_api_key)
if not user:
raise HTTPException(status_code=401, detail="Invalid API key")
return {"user": user, "api_key": x_api_key}
@app.patch("/users/{user_id}")
def update_user(user_id: int, update: UserUpdate, current=Depends(get_current_user)):
# Vulnerable: directly using update.dict() can include unexpected fields
data = update.dict(exclude_unset=False)
# In a real app, this would call a database with data; attacker can set is_admin if present
return {"user_id": user_id, "update": data}
An attacker with a valid API key can send {"is_admin": true} even though is_admin is not intended to be client-updatable. Because the endpoint trusts the API key for authentication and does not explicitly restrict which fields can be modified, mass assignment is possible. The scanner checks for such mismatches between authentication mechanisms and input handling, highlighting cases where API-key-authenticated endpoints accept broad payloads without field-level authorization.
Another scenario involves query parameters or headers bound to models inadvertently. FastAPI’s dependency system can inject values into path operations; if those values are merged into a model without strict field control, mass assignment can occur across sources. The LLM/AI security checks do not apply here, but the scanner’s BOLA/IDOR and Property Authorization checks can surface these overly permissive mappings.
Api Keys-Specific Remediation in Fastapi — concrete code fixes
Remediation centers on explicit allowlisting of fields and strict separation of authentication from authorization. Do not rely on API keys alone to decide what a user can modify. Use Pydantic models with only the fields a client may legitimately update, and validate or map sensitive fields server-side based on the authenticated principal derived from the API key.
Secure endpoint example with field filtering:
from fastapi import FastAPI, Depends, HTTPException, Header
from pydantic import BaseModel, validator
from typing import Optional
app = FastAPI()
VALID_API_KEYS = {"secret-key-123": "alice"}
class UserUpdate(BaseModel):
username: Optional[str] = None
email: Optional[str] = None
@validator("*")
def reject_none_or_empty(cls, v):
if v is None:
return v
if isinstance(v, str) and v.strip() == "":
raise ValueError("string must not be empty")
return v
def get_current_user(x_api_key: str = Header(...)):
user = VALID_API_KEYS.get(x_api_key)
if not user:
raise HTTPException(status_code=401, detail="Invalid API key")
return {"user": user}
def authorized_to_update(current_user: dict, target_user_id: int, update: dict):
# Map API-key identity to user_id in real app; simplified here
# Ensure current_user is allowed to modify only permitted fields
allowed = {"username", "email"}
disallowed = set(update.keys()) - allowed
if disallowed:
raise HTTPException(status_code=403, detail=f"Fields not allowed: {disallowed}")
@app.patch("/users/{user_id}")
def update_user(
user_id: int,
update: UserUpdate,
current: dict = Depends(get_current_user)
):
authorized_to_update(current, user_id, update.dict(exclude_unset=True))
# Apply updates to database using only allowed fields
# For example:
# db_user = get_user(user_id)
# if "username" in update.dict(exclude_unset=True):
# db_user.username = update.username
return {"user_id": user_id, "update_applied": update.dict(exclude_unset=True)}
Key remediation practices:
- Define input models with only the fields clients are permitted to set.
- Use validators to reject empty or malformed values.
- Implement server-side mapping of the API key to a user identity and enforce per-field authorization before applying updates.
- Never pass the raw client dict directly to database models; explicitly map allowed fields.
The CLI tool can be used to verify that endpoints with API key authentication do not accept mass-assignable fields, and the GitHub Action can enforce that new endpoints include such field filtering before merging.
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |