9 min read

Authorization Testing

Authorization flaws are the most common and critical API vulnerabilities. They occur when the API fails to verify if the authenticated user has permission to access the requested resource — an object out of scope (BOLA), a function meant for another role (BFLA), or a property that was never meant to be writable (04 — Mass Assignment).

Authorization boundary

Access-control testing means touching other people's records: use accounts and fixtures you created for the engagement, cap enumeration so you do not walk a customer list, and report the smallest proof — one object, one field, one request — rather than a dump.

BOLA Attack Flow

The diagram is wider than this screen — swipe it sideways to read the labels.

BOLA - Broken Object Level Authorization

BOLA occurs when an API doesn't verify if the user has permission to access a specific object (like a user record, order, or document) based on the ID provided in the request.

1. Identify endpoints with IDs

Look for endpoints that take an ID as a parameter.

Your own objects (baseline)
GET /api/users/1001
GET /api/orders/5001
GET /api/documents/doc-abc

2. Change IDs

Try changing the ID to access another user's data.

The same requests as another user
GET /api/users/1002
GET /api/orders/5002
GET /api/documents/doc-xyz

3. Test different ID formats

Sometimes APIs support multiple ID formats. A UUID is still an identifier — it is longer, not authorized.

ID format variations
GET /api/users/1             # Sequential integer
GET /api/users/00001         # Padded integer
GET /api/users/user-uuid-here # UUID
GET /api/users/admin         # Username as ID
GET /api/users/user@email.com # Email as ID

4. Collect identifiers you were already given

Enumeration is the slow path. Most BOLA is found with an identifier the application handed you: a share link, an export, an invitation, an email notification, a GraphQL node(id:), a mobile API response for a friend or a colleague. Harvest those first, then replay each one with a second account of the same privilege.

Use two accounts, not one

Two same-privilege accounts is the only reliable test: account A's token against account B's object must fail. Burp's Autorize extension replays every request with the second session and flags responses that look like the original — it finds missing checks quickly, but it cannot tell you whether that object was supposed to be visible, so confirm each hit by hand.

5. Test all CRUD operations

Don't just test GET requests. Try to modify or delete other users' data.

All methods on one object
GET    /api/users/1002
PUT    /api/users/1002
DELETE /api/users/1002
PATCH  /api/users/1002

The same swap applies to identifiers inside the body ({"ownerId": 1002}), inside nested objects, and inside GraphQL arguments — the URL is only one of the places an object reference hides.

The control: ownership is part of the query

object-level-check.js
// The ownership predicate is part of the query, not a check after it.
app.get('/api/users/:id', async (req, res) => {
  const caller = req.user;                       // from the verified token
  const row = await db.oneOrNone(
    'SELECT * FROM users WHERE id = $1 AND tenant_id = $2',
    [req.params.id, caller.tenantId]             // never req.body.tenantId
  );
  if (!row) return res.sendStatus(404);          // 404, not 403: no existence oracle
  res.json(pickPublicFields(row));               // see 04 for the response shaping
});

UUIDs do not authorize anything

"Use random, unpredictable IDs instead of sequential integers" is the canonical BOLA misconception and should not appear in a report. A UUID that the application hands out in a share link, an export, an invitation email, a GraphQL node(id:) or another user's API response authorizes exactly as much as 1 does: the attacker does not need to guess an identifier they were given. Random IDs raise the cost of blind enumeration — a mild defence-in-depth note at most — and change nothing about whether the object is checked against the caller.

BFLA - Broken Function Level Authorization

BFLA occurs when an API doesn't verify if the user has permission to access a specific function (like an admin endpoint).

1. Identify privileged endpoints

Look for endpoints that suggest administrative functionality.

  • /api/admin/users
  • /api/admin/config
  • /api/admin/logs
  • /api/internal/debug
  • /api/management/settings
  • /api/v1/admin/export

2. Access with low privileges

Try to access these endpoints with a regular user token.

User token against an admin route
curl -H "Authorization: Bearer $USER_TOKEN" \
  https://api.target.com/api/admin/users

3. HTTP method manipulation

If GET is blocked, try other methods like POST or PUT — and try method-override headers, which some frameworks honour after the router has already run its check.

Method override against admin routes
curl -X POST -H "Authorization: Bearer $USER_TOKEN" \
  https://api.target.com/api/admin/users

curl -X POST -H "X-HTTP-Method-Override: DELETE" \
  -H "Authorization: Bearer $USER_TOKEN" \
  https://api.target.com/api/admin/users/1002

4. Path manipulation

Try to bypass filters by manipulating the URL path.

  • /api/users/../admin/config
  • /api/v1/users/../../admin/users
  • /api/ADMIN/users
  • /api/Admin/users

5. Property-level escalation

Sending {"role": "admin"} at registration, or PUT /api/users/1001 with an extra property, is an API3 test rather than BFLA — payloads and controls live in 04 — Mass Assignment.

API3

The control: a deny-by-default route table

route-policy.js
// Deny by default. A route with no entry is not callable.
const ROUTE_POLICY = {
  'GET    /api/users/:id':        ['user', 'admin'],
  'GET    /api/admin/users':      ['admin'],
  'DELETE /api/admin/users/:id':  ['admin'],
};

function requireRole(req, res, next) {
  const allowed = ROUTE_POLICY[`${req.method} ${req.route.path}`] || [];
  if (!allowed.includes(req.user.role)) return res.sendStatus(403);
  next();
}

Evidence Collection

A BOLA or BFLA finding needs both sides of the swap:

The pair of requests: Account A's request and account B's object identifier, with both full responses — status line, body and the object id each response carried.

Identities: Which two accounts you used, their roles and tenancy, and the fact that neither is privileged beyond the other.

For BFLA, the low-privilege token and the exact response from the admin route, including whether any state changed (DELETE, PUT) — prove the action, not just the 200.

False Positive Identification

  • The object is genuinely shared: Organisation members, support staff and delegated admins are entitled to see the record. Read the access model before the request: a shared document is only a finding if the caller is outside the sharing group.
  • A 403 on a route that never existed: Gateways and CDNs return 403 for unknown paths, so "admin route reachable with a user token" needs a control: confirm the same route with an admin token returns real content, not the same generic error page.

Testing Checklist

Object level (BOLA)

  • ☐ Harvest identifiers the app handed you — share links, exports, invitations, notification emails — then replay each with a second same-privilege account.
  • ☐ Repeat the swap for every method on the route, including body and GraphQL argument identifiers.

Function level (BFLA)

  • ☐ Call every admin and internal route with a low-privilege token, then with method overrides, case variants and trailing slashes.
  • ☐ Record which roles the route actually admits, so the report states the delta rather than "admin endpoint accessible".

Remediation & Defense

Both categories need the same structural answer: one policy, enforced where the data is read or written.

Object level (BOLA)

  • • Authorize on every object access, in the data layer. The ownership or tenancy predicate belongs in the query that fetches the object — WHERE id = ? AND tenant_id = ? — not in the router as "this is a user route, so it is fine". A route-level check cannot know which object the handler will load.
  • • Derive the owner from the token, never the request. The caller's subject and tenant come from the verified token; any ownerId, tenant or userId in a body, query string or path parameter is untrusted input even when the schema marks it required.
  • • One central policy check. A single canAccess(subject, object, action) used by every handler, denying by default — copies of the check drift until someone swaps an ID.
  • • A regression test per endpoint, two accounts. For every route that takes an object reference, assert account A's token gets 403 or 404 on account B's object, for each method, in CI.

Function level (BFLA)

  • • A deny-by-default route table. Enumerate every route and method with the roles allowed to call it; anything not listed returns 403, so adding a route without a policy entry fails closed.
  • • A server-side role check per HTTP method. One path is several functions — GET /admin/users may be support-readable while DELETE /admin/users/:id is admin-only — so the check keys on route and method, in middleware a handler cannot skip.
  • • Test the matrix in CI. Role × route × method, asserting 403 for the roles that must not reach it, including method-override headers, case variants and trailing slashes.
  • • Do not ship the admin surface to clients. A separate host with its own authentication is a far smaller attack surface than a prefix that depends on one middleware being right.

BOLA is the #1 API vulnerability

BOLA (aka IDOR) consistently tops the OWASP API list because logic flaws are not caught by scanners. Always test with two accounts at the same privilege level — swap IDs and object references between them. Automated tools miss most BOLA because they don't understand business context.

Authorization Testing Practice

Exploit BOLA and BFLA vulnerabilities in purpose-built vulnerable API environments.

vAPI — BOLA/IDOR Challenges Custom Lab medium
Object ID manipulationHorizontal privilege escalationTwo-account replay
crAPI — BFLA Admin Functions Custom Lab medium
HTTP method tamperingAdmin endpoint accessRole parameter injection