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/1337where 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.