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_session

The 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_session

If 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.