Lesson 4 · Reading · 30 min
Function-level access and method swaps
IDOR is about reaching an object that is not yours. This lesson is about reaching a function that is not for your role, the admin action a normal member should never be able to call. The application usually "protects" it by hiding the button, and hiding a button is not protection.
The bug: the menu is hidden, the endpoint is open
Picture an app with normal members and admins. Admins get an "Admin" menu with "Manage users", "Delete any post", "Change roles". Members do not see that menu. Behind the menu are endpoints: GET /admin/users, POST /admin/users/9/role, DELETE /posts/500.
The mistake is enforcing the rule only by hiding the menu. The member cannot see the link, so the developer feels the function is protected. But the endpoint is still there, and it still answers. A member who simply sends the request, with no admin link ever shown to them, reaches the admin function, because the server checked "is this a logged-in user?" and forgot to check "is this user an admin?"
This is BFLA (Broken Function Level Authorization): function-level authorization, failing. It is the vertical cousin of IDOR, and you find it the same way, by making the request the interface would not let you make.
Finding the endpoints you were never shown
You cannot call an admin endpoint you do not know about, so first you have to find them. This is where the recon path pays off:
- Read the JavaScript. Front-end code often contains every route the app knows, including admin ones, gated by a client-side
if (user.isAdmin). The route is right there; theifis only in the browser. - Log in as an admin if you can (on your own test instance or a lab that gives you one), map every admin action and its request, then replay those as the member. This is the two-account method turned vertical: capture as admin, replay as member.
- Content discovery. Fuzz for
/admin,/api/admin,/manage,/internal. A403still proves the endpoint exists. - Guess by symmetry. If
GET /api/users/meexists, tryGET /api/users/allor/api/admin/users.
Then, as the low-privilege account, call each one and read the response. A 200 where you expected a 403 is the finding.
The method swap: same URL, different verb
Here is the sharp trick that closes this lesson. Some applications enforce authorization per HTTP method and get it inconsistent. A rule might block GET /admin/report for members but leave POST, HEAD, or PUT on the same path unguarded, because whoever wrote the filter only thought about GET.
So whenever a method is refused, ask: is the refusal about who I am, or only about the verb I used? And try the others:
curl -i -X GET https://target/admin/report # 403 for a member curl -i -X POST https://target/admin/report # …still 403? curl -i -X HEAD https://target/admin/report # frameworks sometimes route HEAD to the GET handler
Related swaps that trip up inconsistent servers: changing the content type (form vs JSON), adding a trailing slash or path-case change (/Admin), or using an override header some frameworks honour (X-HTTP-Method-Override: PUT). Each is a cheap test, and each has bypassed access control in the wild.
Mass assignment: the field you were not offered
One special, high-value BFLA-adjacent bug: mass assignment. An update endpoint takes a JSON object and copies its fields onto the record. The form only offered you name and email: but the endpoint also, carelessly, accepts role or isAdmin. So you send:
PATCH /api/users/me {"name": "Bob", "role": "admin"}
and the server writes role = admin because it trusted every field in the body. You just promoted yourself. Whenever you update your own profile, try adding the privileged fields the UI never showed you, role, is_admin, account_type, verified, balance: and watch the response and your subsequent permissions.
The honesty gate
The finding is a member reaching an admin function or gaining a privilege they should not have. Reaching the admin function as an admin is not a bug. Prove it with a low-privilege account you own performing an action reserved for a higher role, and stop at the proof: demonstrate you can call POST /admin/users/{id}/role as a member and that it takes effect on your own test users, not by actually changing real people's roles or deleting real content.
Your lab
Nimbus Workspaces is a multi-tenant application where the boundary between what a normal member and a privileged/other-tenant user may do is not properly enforced on the server. Map what each role's requests look like, then send the privileged request from the account that should not be allowed it. When the server performs an action your role was never meant to reach, you have reproduced broken function-level authorization in a realistic app.
Where this goes next
You have now broken both halves of authorization, objects and functions. The next four lessons turn to the other question, authentication: weak logins, broken password resets, session handling, and forgeable tokens. We start with the bluntest: authentication that barely resists at all.
This lesson has 2 labs to try what you just read.