Broken Object Property Level Authorization
Theory
API3 Broken Object Property Level Authorization is a 2023 consolidation of two 2019 API Top 10 entries: Excessive Data Exposure (API3:2019) and Mass Assignment (API6:2019). Both stem from the same root cause: the API does not restrict which properties of an object a client can read or write.
Excessive Data Exposure โ Reading Too Much
The API returns full model objects and relies on the client to filter what to display. An attacker who calls the endpoint directly (bypassing the app UI) receives all fields, including sensitive ones.
# API returns everything in the User object:
GET /api/v1/profile
{
"id": 2,
"username": "alice",
"email": "alice@example.com",
"password_hash": "$2b$12$abc...", # should never leave the server
"password_plain": "alice123", # definitely should not exist at all
"role": "admin",
"internal_notes": "flag{...}",
"stripe_customer_id": "cus_abc123", # PCI data
"ssn": "123-45-6789"
}
# The mobile app only shows username and email.
# The API returns everything. Burp Suite captures everything.
Mass Assignment โ Writing Too Much
The API accepts any JSON field from the client and maps it to the model. An attacker adds fields like role, balance, or is_verified that should only be set server-side.
# Normal update request:
PATCH /api/v1/profile
{"display_name": "alice_hacker"}
# Attack โ add sensitive fields:
PATCH /api/v1/profile
{"display_name": "alice_hacker", "role": "admin", "balance": 999999}
# If the server blindly maps all fields to the model, both are accepted
Vulnerable Code
# VULNERABLE โ returns full ORM object (excessive data exposure)
@app.get("/api/v1/profile")
def get_profile(user = Depends(get_current_user)):
return user # entire User row including password_hash, role, internal notes
# VULNERABLE โ binds all request fields to the model (mass assignment)
@app.patch("/api/v1/profile")
async def update_profile(request: Request, user = Depends(get_current_user), db = Depends(get_db)):
data = await request.json()
for key, value in data.items():
setattr(user, key, value) # accepts role=admin, balance=99999, etc.
db.commit()
Fixed Code
from pydantic import BaseModel
# FIXED โ explicit response schema (only returns safe fields)
class ProfileResponse(BaseModel):
id: int
username: str
email: str
# role, password_hash, internal_notes, etc. are NOT in this schema
@app.get("/api/v1/profile", response_model=ProfileResponse) # FastAPI enforces the schema
def get_profile(user = Depends(get_current_user)):
return user # framework filters out undeclared fields
# FIXED โ strict update schema (only allowed fields)
class ProfileUpdateRequest(BaseModel):
display_name: str | None = None
bio: str | None = None
# role, balance, is_admin, etc. are NOT in this schema
@app.patch("/api/v1/profile")
def update_profile(data: ProfileUpdateRequest, user = Depends(get_current_user), db = Depends(get_db)):
if data.display_name is not None:
user.display_name = data.display_name # only whitelisted fields are set
if data.bio is not None:
user.bio = data.bio
db.commit()
Real-World Breaches
- GitHub (2012) โ Mass assignment in Rails allowed a user to push to any repository by adding a
public_keyto any organisation via a crafted POST request. Called the "Rails mass assignment" incident; led to fundamental Rails security changes. - Twitter (2012) โ A mass assignment vulnerability allowed any user to set their
verifiedstatus totrue - Parler (2021) โ Excessive data exposure in the API returned GPS coordinates, device fingerprints, and user metadata not shown in the app; all scraped before shutdown
How to Fix โ Checklist
- Explicit response schemas โ use Pydantic
response_modelin FastAPI; never return raw ORM objects. List only the fields the client needs. - Explicit request schemas โ use a separate Pydantic model for input; never bind raw JSON to a DB model
- Keep internal fields internal โ fields like
password_hash,role,is_admin,stripe_customer_idshould never appear in API responses - Audit API responses โ use automated tools (e.g.
spectral) to flag any response field that contains sensitive keywords - Column-level access control โ some ORMs support field-level permissions; only expose fields the current user's role is allowed to read
Mass Assignment โ Escalate to Admin via PATCH
The profile update endpoint accepts any JSON field and maps it to the user object. Include "role": "admin" in your PATCH request.
Hint
curl -X PATCH /api/v1/profile \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"display_name":"hacker","role":"admin"}'