Insufficient function level authorization
CWE-285CWE-862OWASP A01:2021Updated September 4, 20264 min read
With insufficient function level authorization, the interface decides what a user sees while the server does not check what they may call. An ordinary user who knows the address of an admin function simply executes it. Hiding it in the menu is presentation; authorisation belongs on the server.
An administrator function that does not appear in the menu is unfindable for an ordinary user, until someone opens the JavaScript bundle in which all of the application’s routes sit neatly listed. From that moment the difference between an ordinary user and an administrator is only whether the server refuses the request. Making sure it does is the whole job.
What is function level authorization?
Every application has functions not intended for everyone: managing users, assigning roles, running exports, changing settings, deleting data. Function level authorization is the check establishing whether the requester may perform this specific action.
Insufficient function level authorization, internationally broken function level authorization or BFLA, means that check happens only in the interface. The button is not shown, the menu item is absent, the route is not in the navigation. But the endpoint exists, is reachable, and executes what it is asked as soon as someone sends the request directly.
The mistake is assuming the frontend is a gatekeeper. It is not: it is a display. What a user sees is decided by the frontend; what a user can do is decided solely by the server. Picture a lift with the button for the executive floor taped over. As long as nobody thinks to remove the tape, it works. But the button is still there, and it still functions.
How is broken function level authorization exploited?
Vulnerable:
// Logged in, but no check on the role
app.post('/api/admin/users/:id/role', requireLogin, async (req, res) => {
await users.setRole(req.params.id, req.body.role);
res.json({ ok: true });
});
In the interface this endpoint is only reachable from an admin screen that ordinary users never see. But the endpoint itself asks only for a valid session. An attacker finds the address in the frontend bundle:
// In the application's JavaScript, visible to everyone
const routes = {
profile: '/api/profile',
manageUsers: '/api/admin/users',
changeRole: '/api/admin/users/:id/role',
};
And then sends, with their own ordinary account:
POST /api/admin/users/4471/role HTTP/1.1
Cookie: sid=<own valid session>
Content-Type: application/json
{"role":"administrator"}
HTTP/1.1 200 OK
They have just made themselves an administrator. No password was guessed, no injection performed and no session stolen; one request was sent to an endpoint that served them politely.
Safe:
// Authorisation per route, explicitly recorded
const requireRole = (...roles) => async (req, res, next) => {
const user = await users.find(req.user.id); // current role
if (!user || !roles.includes(user.role)) {
await auditlog.write('authorisation denied', {
user: req.user.id, route: req.originalUrl,
});
return res.sendStatus(404);
}
next();
};
app.post('/api/admin/users/:id/role',
requireLogin,
requireRole('administrator'),
async (req, res) => {
await users.setRole(req.params.id, req.body.role);
res.json({ ok: true });
},
);
// Safety net: every route without explicit rights is refused
app.use('/api', (req, res) => res.sendStatus(404));
The check now sits on the route itself and uses the current role from the database rather than a value from the session. The safety net at the end matters just as much: a new endpoint where the developer forgets the authorisation therefore closes rather than opens. That is the difference between a mistake noticed during testing and a mistake that becomes a vulnerability in production.
What is the impact of broken function level authorization?
The severity is high to critical, because the outcome usually amounts to elevating one’s own privileges. A user who can make themselves an administrator thereby gains access to everything the application offers.
Even without that direct route the consequences are considerable. Admin functions are by definition the functions with the widest reach: exporting all customer data, deleting records, changing settings that affect security, disabling two-factor authentication for other accounts. One unprotected endpoint from that category is enough for a substantial incident.
What makes it hard to establish afterwards is that everything looks normal. The attacker uses their own valid account, the request is correctly formed and the response is a 200. Without logging of authorisation decisions there is no trace showing that someone called a function not meant for them.
How do you detect broken function level authorization?
A tester first maps all endpoints, and deliberately looks beyond what the interface shows. The frontend JavaScript bundle is the richest source here: it often contains all routes, including those of admin screens. API documentation, older versions and predictable naming are used as well.
Every endpoint found is then called with an account holding minimal rights. If the request succeeds, the finding stands. Each method is tested separately, because authorisation is sometimes present on GET and not on POST or DELETE. The classic detours are tried too: a different spelling of the path, an older API version leading to the same functionality, or a header carrying a role the application wrongly trusts. AssistSec runs this test with accounts in every available role, because the interesting gaps usually sit not between anonymous and logged in but between two levels of logged-in users.
How do you prevent broken function level authorization?
- Check on the server for every endpoint whether the user may perform the associated function.
- Refuse by default and permit only explicitly, so a forgotten check leads to a refused request.
- Group admin functions under their own path with one central role check.
- Fetch the role from the database on every request, not from the session or a token.
- Test every HTTP method separately; authorisation is often missing on one of them.
- Treat hiding buttons and menu items as presentation, never as security.
- Remove or shield older API versions, because those often lack the newer checks.
- Do not trust any role or rights sent along in a header or parameter.
- Log denied authorisation attempts, so reconnaissance by a logged-in user becomes visible.
Sources
Frequently asked questions
What is the difference with BOLA?
BOLA concerns whether you may access a given object; BFLA concerns whether you may perform a given function. With BOLA a customer reaches another customer's invoice, with BFLA an ordinary user performs an administrator function. Both need separate testing.
Is hiding buttons enough?
No, and that is the heart of this finding. What the interface shows is a presentation choice; the server executes what it is asked. Anyone can send a request directly. The only place authorisation counts is the server.
Why are endpoints found that appear nowhere?
From the JavaScript bundle of the frontend, which often contains all routes including the administrator functions. Also from API documentation, from predictable naming and from older versions of the API. Obscurity is not protection.
How do I enforce this structurally?
By making the check refuse by default and permit only explicitly. A central layer blocking every route without recorded rights turns a forgotten check into a failure during testing rather than a vulnerability in production.
Related articles
- VulnerabilitiesCWE-1059A05:2021Outdated API versions remain reachableAn old API version left running beside the new one misses the checks added later. Learn how attackers use that detour.
- VulnerabilitiesCWE-639A01:2021Insufficient object level authorization in APIsAn API that only checks whether you are logged in, not whether this record is yours, hands over other people's data. Learn how to enforce it.
- VulnerabilitiesCWE-284A01:2021Broken access controlBroken access control explained: horizontal and vertical privilege escalation, forced browsing, and how deny by default fixes it server-side.
- VulnerabilitiesCWE-213A01:2021API responses contain too much dataAn API returning whole database records and leaving the filtering to the frontend leaks fields nobody was meant to see.
- VulnerabilitiesCWE-269A01:2021Privilege escalationPrivilege escalation explained: how attackers gain admin rights through a client-trusted role field or an unprotected admin route, and how to stop it.