A01:2025 Broken Access Control
is the first category in OWASP Top 10:2025. The official OWASP A01:2025 page describes it as users acting outside their intended permissions. This category is critical in web applications because modern systems protect data through API endpoints, object IDs, tenant boundaries, role checks, and service-to-service requests. A logged-in user is not automatically authorized for every resource; the real security decision is whether the server verifies which action that user may perform on that specific resource.
Broken access control often appears through simple-looking but high-impact failures: viewing another user’s invoice, calling an admin endpoint as a standard user, changing
tenant_id
to access another customer’s data, or manipulating a backend request to internal resources. In the 2025 release, SSRF is also covered under this category because many SSRF cases let an attacker access internal resources through the server.
Root Cause
Access control is the policy that determines which user can access which resource with which action. This control must be enforced in trusted server-side code, not in the client. Broken access control commonly appears when:
-
An endpoint checks login but not authorization
-
User-controlled
id
,
account_id
,
tenant_id
, or
file_id
values are accepted
-
Admin functions are only hidden in the UI
-
API methods use inconsistent authorization checks
-
Role data from JWTs, cookies, hidden fields, or request headers is trusted without validation
-
Multi-tenant applications do not enforce tenant isolation
Typical Attack Scenario
A user views their own order:
GET /api/orders/1001 HTTP/1.1
Host: vulnerable-app.example
Cookie: session=user_a_sessionThe attacker tries a different order number on the same endpoint:
GET /api/orders/1002 HTTP/1.1
Host: vulnerable-app.example
Cookie: session=user_a_sessionIf the application returns order
1002
without checking whether it belongs to
user_a
, this is an IDOR example.
Vulnerable Code
The following example fetches the record only by
order_id
. It does not validate the relationship between the session user and the record owner.
@app.get("/api/orders/<order_id>")
def get_order(order_id):
order = db.query("SELECT * FROM orders WHERE id = ?", [order_id])
return jsonify(order)Defended Code
The safer approach filters by both
order_id
and the authenticated user’s ID. Special roles such as admin should be defined through a central policy.
@app.get("/api/orders/<order_id>")
def get_order(order_id):
user = current_user()
order = db.query(
"SELECT * FROM orders WHERE id = ? AND owner_id = ?",
[order_id, user.id]
)
if not order:
abort(404)
return jsonify(order)Testing Approach
Access control testing requires more than one user. At minimum, use accounts with different privilege levels:
- Anonymous user
- Standard user
- Different standard user
- Privileged user
- Admin or tenant owner
Compare the same endpoints with each role’s session. Test
GET
,
POST
,
PUT
,
PATCH
, and
DELETE
separately. An application may enforce authorization for
GET
but forget it for
DELETE
.
Defensive Controls
- Use deny-by-default.
- Enforce authorization through central middleware or policy.
- Make resource ownership checks mandatory in the domain model.
- Apply tenant isolation in database queries and service logic.
- Do not trust client-side role, price, limit, or owner data.
- Restrict CORS to trusted origins.
- Log access control failures and alert on repeated attempts.
Reporting Note
A good access control report does not only say “another user’s data is visible.” It shows which role accessed which resource, the expected behavior, the actual response, the affected data type, and the recommended server-side ownership control.