๐Ÿ›ก OWASP Lab
API3:2023 High

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_key to 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 verified status to true
  • 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_model in 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_id should 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
Challenge 1

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"}'