Parameter

Broken function level authorization (BFLA)

Also known as

  • Function-level access control flaw
  • Vertical authorization bypass

Broken function level authorization (BFLA) is an API vulnerability where an endpoint meant for a higher role, such as an admin or support action, does not check the caller's role, so an ordinary user calls it directly and performs privileged operations like managing users or changing settings.

Category
API security
OWASP
API5:2023 Broken Function Level Authorization
Last reviewed

How does BFLA work?

BFLA happens when the API decides who may call a function by what the UI shows, not by a server-side role check. The admin button is hidden from regular users, but the endpoint behind it answers anyone with a valid session.

A regular user's app never shows the "Invite admin" screen. The endpoint still exists:

POST /api/v1/admin/invites HTTP/1.1
Host: api.example
Authorization: Bearer <user_a token, role=member>
Content-Type: application/json

{"invitee": "user_b", "role": "admin"}
HTTP/1.1 201 Created
Content-Type: application/json

{"invite_id": "invite_311", "invitee": "user_b", "role": "admin", "status": "pending"}

A member-role token just created an admin invite. The correct answer was 403 Forbidden. OWASP's API5:2023 entry describes the same pattern: an invite endpoint meant only for administrators that a user reaches by crafting the request themselves.

BOLA is about which object you can touch. BFLA is about which action you can perform. The two often combine: a missing role check on DELETE /api/v1/admin/projects/{id} is BFLA, and if it also accepts any project ID it is BOLA too.

Where do unprotected functions hide?

Privileged functions are usually discoverable from the client, from naming conventions, or from older versions of the API. Anything the server exposes is callable, whether or not a link to it exists.

  • Admin path prefixes. /admin/, /internal/, /manage/, /support/, /ops/. Change /api/v1/users/me to /api/v1/admin/users and see what answers.
  • Method changes. GET /api/v1/users/user_b is allowed for everyone, and the framework also routes PUT and DELETE on the same path to handlers that never got a role check.
  • JavaScript bundles. Single-page apps often ship the admin UI's routes and API calls to every user, gated only by a client-side if (user.isAdmin).
  • Mobile and partner APIs. A separate API for a partner or an internal tool reuses the same backend but was written assuming only trusted callers.
  • Old or undocumented versions. /v1/ kept a check that /v2/ forgot, or the reverse. Unlisted endpoints like these are the subject of shadow APIs.
  • GraphQL mutations. A schema with deleteUser or setUserRole mutations available to every authenticated caller, because authorization was added to the queries and not to the mutations.

How do you test for BFLA?

Build a matrix of every function against every role, then call each function with each role's token. Anything a lower role can do that it should not is a finding.

  1. Get one account per role: anonymous, member, manager, admin, and any support or billing roles. If you only have an admin account, ask for the others; BFLA can't be tested with one role.
  2. Record the admin's traffic. Use every admin feature once through a proxy. This gives you the real request shapes, including bodies and headers.
  3. Add discovered endpoints. Pull routes from the OpenAPI spec, from JS bundles, and from GraphQL introspection. Guess common admin paths next to known ones.
  4. Replay each admin request with lower-role tokens. Change only the Authorization header or cookie. Then try the member request with a different method on the same path.
  5. Classify responses. 401 or 403 is correct. 200, 201 or 204 is a finding. A 400 validation error is suspicious: the request passed authorization and failed later.
  6. Check the effect. Confirm from the admin session whether the action happened: the invite exists, the setting changed. Use test objects you created.
  7. Check role claims. If the role comes from a JWT or a request field, try editing it. That moves the finding toward mass assignment or JWT algorithm confusion.

Write the matrix up as a table in the report; it doubles as the regression test list.

How do you fix BFLA?

Deny by default and require every route to declare the roles it allows. OWASP's guidance for API5 is the same: explicit grants per role, with administrative controllers inheriting a check.

Express, with a route-level guard and a default deny on the admin router:

function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ error: "forbidden" });
    }
    next();
  };
}

const admin = express.Router();
admin.use(requireAuth, requireRole("admin")); // every route below inherits this
admin.post("/invites", createInvite);
admin.delete("/projects/:id", deleteProject);

app.use("/api/v1/admin", admin);

Django REST Framework, with a global default and explicit overrides:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAdminUser"],
}

# views.py: member-facing views opt in explicitly
from rest_framework.permissions import IsAuthenticated

class MyProjectsView(ListAPIView):
    permission_classes = [IsAuthenticated]

With the global default set to the most restrictive class, a new view that forgets to declare permissions is admin-only, not public.

What holds across stacks:

  • Read the role from the server-side session or a verified token. Never from a request field or header the client sets.
  • Check every method on a path, not only the one the UI uses.
  • Keep admin functions on a separate router, service or host where the check is applied once, at the top.
  • Test the role matrix in CI. A test that calls every admin route with a member token and expects 403 catches the next regression.

What does not work: hiding buttons, obscure URLs, and relying on the client to never send the request. A WAF won't catch it either, because the request is well formed and authenticated. BFLA is a form of vertical privilege escalation, and it needs a server-side check to close.

Written by Parameter · Last reviewed

[ related terms ]

Related terms.

Broken object level authorization (BOLA)

Broken object level authorization (BOLA) is an API vulnerability where an endpoint accepts an object ID from the client and returns or changes that object without checking the caller owns it, so an attacker swaps in another user's or tenant's ID and reads, edits or deletes their records.

Privilege escalation

Privilege escalation is gaining access or permissions beyond what an account was granted: reaching another user's data at the same level (horizontal) or performing actions reserved for a higher role such as admin (vertical), usually because the server does not check authorization on every request.

Mass assignment

Mass assignment is a vulnerability where a framework copies every field in a request body onto a data model, so an attacker adds fields the form never offered, such as role, plan or owner_id, and changes properties they should not control, often granting themselves higher privileges.

Shadow API

A shadow API is an API endpoint, version or host that runs in production but is missing from the organization's inventory and documentation, so it skips the security reviews, gateway policies and monitoring applied to known APIs and often exposes old or unprotected functionality to anyone who finds it.

JWT algorithm confusion

JWT algorithm confusion is a vulnerability where an attacker changes a token's alg header so the server verifies it with the wrong algorithm, signing an RS256 token with the public key as an HMAC secret or setting alg to none, and forging a valid token.