Back to Cheat Sheets

πŸšͺ Broken Function Level AuthorizationAPI 2019

OWASP API Security Top 10 2019 - API5

HIGH RISK

πŸ“‹ What Is It?

Broken Function Level Authorization (BFLA) is the failure to check whether the caller is allowed to invoke a particular operation. Where BOLA is about which records you may touch, BFLA is about which actions you may perform: promoting a user, deleting a tenant, exporting billing, calling /admin/*, or using a method (DELETE) the UI never offers you.

The classic cause is hiding instead of enforcing: the front end simply doesn't render the admin button, and the endpoint behind it assumes nobody will ever call it. Attackers read the JavaScript bundle, the OpenAPI spec, or just guess - and call it directly.

API5 OWASP Rank 2019
Admin Typical Target
Privilege Escalation Impact

⚠️ Common Exploits

  • Guessed Admin Routes: /api/admin/users, /api/v1/internal/*
  • Method Swap: GET is allowed for all, DELETE on the same path is unguarded
  • UI-Only Controls: the button is hidden; the endpoint is not protected
  • Role from the Client: server trusts role in the body, header, or an unverified token
  • Version Drift: /v1 still live and unguarded while /v2 is hardened
  • Spec Leakage: Swagger/GraphQL introspection publishes every privileged operation
  • Default-Allow Routing: new handlers ship without the authorization decorator

πŸ”΄ Attack Flow

1. Log in as an ordinary user and map the API (JS bundle, spec, traffic)
↓
2. Look for privileged verbs and paths the UI never calls for you
↓
3. Call them with your own valid token
↓
4. Vary the method and the version prefix where blocked
↓
5. IMPACT: Privilege escalation - administrative actions from a basic account!

❌ Vulnerable Code

# Bad: authenticated is not the same as authorised @app.route('/api/admin/users/<int:uid>', methods=['DELETE']) @requires_auth ← any logged-in user reaches this def delete_user(uid): User.query.get(uid).delete() // Bad: the control lives in the front end {user.isAdmin && <button onClick={() => api.post('/api/users/promote')}>Promote</button>} // …and the endpoint itself checks nothing // Bad: role taken from the request if (req.headers['x-user-role'] === 'admin') { /* … */ } ← attacker sets the header # Bad: only the new version was hardened # /api/v2/reports β†’ @requires_role('admin') # /api/v1/reports β†’ still deployed, no checks at all

βœ… Secure Code

# Good: deny by default, then grant explicitly @app.before_request def enforce(): rule = policy_for(request.endpoint) if rule is None: abort(403) # an unlisted route is forbidden, not open if not rule.allows(g.current_user): abort(404) # do not confirm the route exists @app.route('/api/admin/users/<int:uid>', methods=['DELETE']) @requires_role('admin') # explicit, server-side, per operation def delete_user(uid): ... // Good: roles and permissions come from the verified token / server store const can = (user, action) => rbac.permissions(user.sub).includes(action); if (!can(req.user, 'user:promote')) return res.sendStatus(404); // Good: the UI mirrors the policy, it does not implement it {permissions.includes('user:promote') && <PromoteButton />} // convenience only # Good: retire old versions; test every method on every route # CI: for each route Γ— each method Γ— each role β†’ assert the expected status

βœ“ Prevention Checklist

  • Deny by default - an unmapped route or method must be forbidden
  • Enforce authorization server-side on every operation, never in the UI
  • Derive roles and permissions from the server, not from client input
  • Cover every HTTP method on every path, not just the ones the app uses
  • Separate administrative APIs and gate them by role and network
  • Keep one central policy (RBAC/ABAC) rather than per-handler checks
  • Decommission old API versions; do not leave /v1 unguarded
  • Restrict Swagger/GraphQL introspection in production
  • Return 404 for unauthorised privileged routes
  • Add CI tests asserting a normal user gets 403/404 on every admin route

πŸ” Detection & Tools

Testing:

Burp Suite (Autorize) OWASP ZAP ffuf / kiterunner Schemathesis

Policy:

OPA / Cedar Casbin API gateway authz

How to Test:

  • Replay every admin request with a low-privilege token
  • Fuzz for /admin, /internal, /debug, /v1 paths
  • Try each method (PUT, PATCH, DELETE) on known paths
  • Read the front-end bundle for endpoints your role never calls
  • Check that older API versions enforce the same policy

🌍 Real-World

These are documented incident classes - recurring patterns rather than single CVEs.

  • Admin endpoints reachable by any user: the SPA hides the admin panel, but POST /api/admin/* answers to any valid token.
  • Self-promotion to admin: a role-change operation intended for staff is callable by the account whose role it changes.
  • Method gaps: read is authorised, but the matching DELETE handler was added later without the decorator.
  • Forgotten API versions: a deprecated /v1 kept alive for one legacy client bypasses every control added in /v2.
  • Client-supplied roles: a header or body field naming the caller's role is trusted by the server.

πŸ“Œ Quick Tips

  • DO NOT treat a hidden UI control as a security control
  • DO NOT read the caller's role from the request
  • DO deny by default and grant per operation
  • DO test every method Γ— route Γ— role in CI
  • DO retire old versions and internal routes

πŸ“œ Compliance

Related Standards:

  • OWASP API Security Top 10 2019 - API5
  • Maps to API5:2023 (unchanged)
  • CWE-285 Improper Authorization
  • CWE-862 Missing Authorization
  • CWE-863 Incorrect Authorization