๐Ÿ›ก OWASP Lab
API1:2023 Critical

Broken Object Level Authorization

Theory

API1 BOLA (Broken Object Level Authorization), also known as IDOR (Insecure Direct Object Reference), is the #1 API vulnerability and the most consistently exploited class in bug bounty programs. APIs expose endpoints that operate on object IDs supplied by the client. If the server doesn't verify who is making the request against who owns the object, any authenticated user can access any other user's data by simply changing an ID.

Why APIs Are Especially Vulnerable

  • RESTful design exposes IDs everywhere โ€” GET /orders/42, GET /users/7/profile, GET /messages/1337. Every number is a potential IDOR.
  • Mobile apps expose the API โ€” traffic can be intercepted with a proxy (Burp Suite, mitmproxy). Sequential IDs are trivial to enumerate.
  • Frameworks don't enforce ownership โ€” an ORM that fetches db.get(Order, order_id) is correct code that does the wrong thing. The developer must add the ownership check manually.
  • Testing is often account-only โ€” QA tests with one account and never tries cross-account access

BOLA Attack Pattern

# Step 1: Authenticate as alice and observe a normal response
curl -X POST /api/v1/auth/login   -H 'Content-Type: application/json'   -d '{"username":"alice","password":"alice123"}'
# Response: {"token": "Bearer eyJ..."}

# Step 2: Fetch alice's own order โ€” note the ID
curl -H 'Authorization: Bearer eyJ...' /api/v1/orders/3
# Response: {"id": 3, "user_id": 2, "product": "Widget"}

# Step 3: Change the ID โ€” the server never checks ownership
curl -H 'Authorization: Bearer eyJ...' /api/v1/orders/2
# Response: {"id": 2, "user_id": 1, "secret_note": "flag{...}"}
# The server accepted alice's token but returned ADMIN's order!

Vulnerable Code

# VULNERABLE โ€” order_id is fully user-controlled; ownership not checked
@app.get("/api/v1/orders/{order_id}")
def get_order(order_id: int, user = Depends(get_current_user), db = Depends(get_db)):
    order = db.get(Order, order_id)   # fetches ANY order
    if not order:
        raise HTTPException(404)
    return order   # returns another user's data with no complaint

Fixed Code

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

Real-World Bug Bounty Payouts

  • HackerOne #125410 โ€” IDOR on Shopify Partner API; attacker could read any store's orders. $5,000 bounty.
  • HackerOne #322914 โ€” IDOR on Uber API; reading any driver's trip details. $10,000 bounty.
  • Starbucks (2019) โ€” IDOR on the rewards API allowed reading any customer's transaction history and stored card balances by changing a customer ID
  • USPS (2018) โ€” An API endpoint returned any user's profile data based on a supplied email address; 60 million records exposed

How to Fix โ€” Checklist

  • Enforce ownership in the query โ€” WHERE id = :id AND user_id = :current_user_id; the database filter guarantees ownership
  • Test cross-account access โ€” automated test: user A obtains a resource ID, user B tries to access it. This should return 404 or 403.
  • Use UUIDs for IDs exposed in the API โ€” not a fix on its own, but makes enumeration much harder
  • Centralise ownership checks โ€” create a shared get_owned_resource() helper; don't duplicate the check in every route
  • Log access-denied events โ€” repeated 403/404 on sequential IDs signals active IDOR enumeration
Challenge 1

BOLA โ€” Read Another User's Order

Log in as alice, then try to access order ID 2 (which belongs to admin). The server does not check whether the order belongs to the authenticated user.

Hint
1. POST /api/v1/auth/login with {"username":"alice","password":"alice123"}
2. GET /api/v1/orders/2 with the returned token