๐Ÿ›ก OWASP Lab
A01:2021 Critical

Broken Access Control

Theory

Broken Access Control moved from 5th place (OWASP 2017) to #1 in 2021 โ€” found in 94% of tested applications. Access control enforces that users can only act within their intended permissions. When it fails, attackers can read other users' data, call admin-only functions, or escalate their own privileges.

Types of Broken Access Control

  • IDOR (Insecure Direct Object Reference) โ€” changing a resource ID in a URL to access another user's record: GET /orders/1337 where 1337 is not your order
  • Missing Function-Level Access Control โ€” admin panels or API routes are reachable with zero auth checks; the route simply exists and returns data
  • Path Traversal โ€” escaping the intended directory: GET /download?file=../../etc/passwd
  • Horizontal Privilege Escalation โ€” user A can read/modify user B's data (same role, different account)
  • Vertical Privilege Escalation โ€” a regular user reaches admin-only actions (different role)
  • CORS Misconfiguration โ€” Access-Control-Allow-Origin: * on sensitive endpoints lets any origin read the response

Real-World Breaches

  • First American Financial (2019) โ€” 885 million mortgage documents leaked; anyone could view any document by incrementing a number in the URL. No authentication required.
  • Parler (2021) โ€” All 70 million posts were scraped in bulk before shutdown because post IDs were sequential and no access check existed on the API
  • Facebook (2018) โ€” An IDOR in the video upload flow allowed any logged-in user to access any other user's private videos
  • Venmo (2019) โ€” The public transaction API required no authentication, exposing 207 million transactions including payees and amounts

Vulnerable Code Pattern

# VULNERABLE โ€” no ownership check; any authenticated user can read any order
@app.get("/orders/{order_id}")
def get_order(order_id: int, current_user = Depends(get_current_user)):
    order = db.get(Order, order_id)   # fetches ANY row โ€” never checks ownership!
    if not order:
        raise HTTPException(404)
    return order   # leaks data belonging to other users

How the Attack Works โ€” Step by Step

# Attacker: alice (user_id=2). Target: admin order (user_id=1)

# Step 1 โ€” observe your own order to learn the ID format
GET /orders/5  โ†’  {"id": 5, "user_id": 2, "product": "Widget"}

# Step 2 โ€” enumerate other IDs; the server never checks ownership
GET /orders/1  โ†’  {"id": 1, "user_id": 1, "secret_note": "flag{...}"}
GET /orders/2  โ†’  {"id": 2, "user_id": 1, "product": "Confidential"}

# Automate with a simple loop:
for i in range(1, 1000):
    r = requests.get(f"https://target.com/orders/{i}", headers=auth)
    if r.status_code == 200:
        print(i, r.json())

Fixed Code

# FIXED โ€” ownership enforced inside the database query
@app.get("/orders/{order_id}")
def get_order(order_id: int, current_user = Depends(get_current_user)):
    order = db.exec(
        select(Order)
        .where(Order.id == order_id)
        .where(Order.user_id == current_user.id)   # ownership in the WHERE clause
    ).first()
    if not order:
        raise HTTPException(404)   # same 404 for not-found AND not-owned โ€” no info leak
    return order

# For admin routes, enforce via a reusable dependency:
def require_admin(user = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(403)
    return user

@app.get("/admin/users")
def list_all_users(_admin = Depends(require_admin)):   # fails fast if not admin
    return db.exec(select(User)).all()

How to Fix โ€” Checklist

  • Deny by default โ€” every route requires authentication unless explicitly marked public
  • Enforce ownership in the query โ€” always filter by user_id = current_user.id; never trust a client-supplied ID alone
  • Use indirect references โ€” UUIDs instead of sequential integers make enumeration harder (but are not a substitute for proper auth checks)
  • Two-account test โ€” during every code review: can user A access user B's resources? Automate this check in CI
  • Log & alert on failures โ€” repeated 403/404 on sequential IDs is a sign of IDOR scanning
Challenge 1

IDOR โ€” Read another user's private profile

The profile endpoint returns any user's data (including private notes) based on a URL parameter. No authorisation check is performed. Log in as alice (user_id=2) and try to view admin's profile.

Hint
Try changing user_id to 1 in the URL.
Challenge 2

Missing Function-Level Access Control โ€” Unauthenticated Admin Panel

The admin panel has no authentication check. Any user (or no user at all) can access it directly.

Hint
Visit /web/a01/admin directly.