The Keys to the Kingdom Are Hanging on the Door
In 2023, a security researcher found they could access any Optus customer's data by simply changing a number in an API request. No authentication bypass. No SQL injection. Just incrementing an ID from 1001 to 1002. That single flaw exposed 10 million customer records.
This is Broken Access Control — and it's been sitting at #1 on the OWASP Top 10 since 2021. In 2025, it's still there. Still devastating. Still everywhere.
What Is Broken Access Control?
Access control enforces who can do what. When it's broken, users can act outside their intended permissions — viewing other users' data, modifying records they shouldn't touch, or escalating to admin functionality.
It's not about getting in (that's authentication). It's about what you can do once you're in (authorization).
Common patterns we see:
- IDOR (Insecure Direct Object References) — Changing
/api/user/123to/api/user/124 - Privilege escalation — Regular user accessing
/admin/dashboard - Missing function-level access control — Deleting resources without ownership checks
- Metadata manipulation — Tampering with JWTs or hidden form fields
The Attacker's View: Finding and Exploiting BAC
As pentesters, Broken Access Control is often our highest-value target. Here's the methodology:
Step 1: Map the Application with Two Accounts
Create two accounts with different privilege levels (e.g., user_a and user_b, or user and admin). Log every request both accounts can make.
Step 2: Hunt for IDORs in Burp Suite
Capture a request that fetches user-specific data:
GET /api/v1/orders/7841 HTTP/1.1
Host: target.com
Authorization: Bearer eyJhbG...user_a_token
Now, swap the token to user_b's session and replay. Does it return user_a's order? That's an IDOR.
Step 3: Force Browse to Privileged Endpoints
Try accessing admin routes directly:
GET /admin/users HTTP/1.1
Authorization: Bearer eyJhbG...regular_user_token
No redirect to login? You've got missing function-level access control.
Step 4: Tamper with State-Changing Requests
Check if you can modify resources you don't own:
DELETE /api/v1/posts/9921 HTTP/1.1
Authorization: Bearer eyJhbG...attacker_token
If post 9921 belongs to someone else but gets deleted — that's a critical finding.
Real-World Damage: The Optus Breach (2022)
The Optus breach wasn't sophisticated. An unauthenticated API endpoint exposed customer data via sequential IDs. Attackers scraped names, dates of birth, passport numbers, and driver's licenses for 9.8 million Australians.
Cost to Optus:
- $140 million AUD in remediation
- Class action lawsuits
- CEO resignation
- Regulatory investigation
The fix? A few lines of authorization code.
Try It Yourself: Vulnerable Flask App
Save this as vulnerable_app.py and run it locally:
from flask import Flask, request, jsonify
app = Flask(__name__)
# Simulated database
users = {
"alice": {"id": 1, "email": "alice@example.com", "role": "user"},
"bob": {"id": 2, "email": "bob@example.com", "role": "admin"},
}
orders = {
101: {"user_id": 1, "item": "Laptop", "total": 1200},
102: {"user_id": 2, "item": "Phone", "total": 800},
}
@app.route("/api/orders/<int:order_id>")
def get_order(order_id):
# BUG: No check if the requesting user owns this order!
order = orders.get(order_id)
if order:
return jsonify(order)
return jsonify({"error": "Not found"}), 404
@app.route("/admin/users")
def admin_users():
# BUG: No check if the user is actually an admin!
return jsonify(users)
if __name__ == "__main__":
app.run(debug=True, port=5000)
Exploit it:
# As any user, access another user's order (IDOR)
curl http://localhost:5000/api/orders/102
# As any user, access the admin endpoint
curl http://localhost:5000/admin/users
Both requests succeed when they shouldn't.
Fix It: Secure Code Patterns
For IDORs — Always validate ownership:
@app.route("/api/orders/<int:order_id>")
def get_order(order_id):
current_user_id = get_current_user_id() # From session/JWT
order = orders.get(order_id)
if not order:
return jsonify({"error": "Not found"}), 404
if order["user_id"] != current_user_id:
return jsonify({"error": "Forbidden"}), 403
return jsonify(order)
For privileged endpoints — Check roles explicitly:
@app.route("/admin/users")
def admin_users():
current_user = get_current_user()
if current_user["role"] != "admin":
return jsonify({"error": "Forbidden"}), 403
return jsonify(users)
General principles:
- Deny by default — require explicit permission grants
- Validate on every request (never trust client-side checks)
- Use indirect references (UUIDs) instead of sequential IDs where possible
- Log and alert on access control failures
Key Takeaway
Broken Access Control isn't glamorous. There's no fancy payload or zero-day exploit. It's just... checking whether someone should be allowed to do something. And yet it's the #1 vulnerability year after year because developers assume the UI will enforce the rules.
As pentesters, our job is to prove that assumption wrong.
Next: We tackle Injection — because user input should never be trusted.