Broken Access Control: IDOR, BOLA, and Why Scanners Miss Them
Broken access control is the most common serious finding in modern applications and the hardest class for tooling to detect. The reason is uncomfortable: a successful exploit does not look like an attack. It looks like a valid, authenticated, well-formed request that returns a normal response. There is no payload to flag, no error to catch, and no anomaly in the logs.
The three levels at which it breaks
Object level: IDOR and BOLA
You request a record that is not yours and the server returns it. The two acronyms describe the same failure from different traditions: IDOR, insecure direct object reference, comes from web application testing, and BOLA, broken object level authorization, is the API Top 10 name and now the more precise term. Both mean the endpoint checked that you are logged in and forgot to check that the object belongs to you.
The classic form is a sequential integer in a URL. The more interesting forms are not: an identifier in a JSON body, a customer number in a header, a filename in an export request, or a UUID that looks unguessable but is leaked by a different endpoint that lists them. Switching to UUIDs is not a fix, it is a speed bump.
Function level
A standard user calls an administrative endpoint directly and it works, because the only thing preventing it was that the interface did not render the button. Authorization was implemented in the front end, which is not a place authorization can be implemented. Internal tooling and support consoles are the usual offenders, because they were built for trusted staff and later exposed on the same API surface as customer routes.
Field level: mass assignment
The endpoint is legitimately yours to call, but the request body binds more fields than intended. Updating your display name also lets you set your role, your account tier, or your organization identifier, because the model bound every field it received. This one is a framework convenience turning into a privilege escalation, and it is found by sending fields that were not in the documentation.
Why automated tools cannot find these
A scanner works by sending inputs and recognizing signatures in responses: an error message, a stack trace, a timing difference, reflected content. Broken access control produces none of those. When an attacker requests invoice 1338 instead of 1337 and receives it, the response is a clean 200 containing well-formed JSON.
To know that response was wrong, you have to know that invoice 1338 belongs to a different customer, that the authenticated caller has no relationship to that customer, and that the application intended to prevent exactly this. That is semantic knowledge about the business, and it is why this class requires a human with at least two accounts. Any test performed with a single set of credentials cannot find it at all, which is worth remembering when reading a clean report.
How to test it properly
Get two tenants and two privilege levels within each, then work systematically rather than by sampling. For every endpoint that accepts an identifier, attempt the operation as the wrong tenant and as the lower-privileged role, for read, write, and delete. Enumerate identifiers where you can and infer them where you cannot. Send undocumented fields. Call administrative routes from a standard session.
The systematic part matters because access control is usually inconsistent rather than absent. A codebase will have the check on twelve endpoints and miss it on the thirteenth, and sampling four endpoints has a good chance of hitting only the twelve. When you do find one, assume it is a pattern and check the whole surface, then report it as a pattern so the fix addresses the cause rather than one route.
How to fix it, and what does not work
The durable fix is centralized, server-side authorization enforced at the data access layer rather than sprinkled through controllers. Every query that fetches an object should require the requesting principal and filter by ownership as a matter of course, so that forgetting the check fails closed instead of open. Deny by default, and make the ownership predicate part of the query rather than a separate step somebody can omit.
What does not work: unguessable identifiers, hiding functionality in the user interface, checking authorization in the client, rate limiting as a substitute for authorization, and relying on an API gateway that validates the token but knows nothing about object ownership. All of these are commonly proposed and none of them addresses the actual defect.
Severity is about business impact, not technique
A single unguarded endpoint that leaks the entire customer base is Critical, even though the technique is trivial and the CVSS base score computed mechanically might land lower. We rate these on where the chain ends and explain the reasoning in the impact section, which is the approach described in our testing methodology. Regulators and customers do not care that the request was well formed.
Where BD Emerson fits
This class is the centre of gravity in our API penetration testing and web application penetration testing, which is why we ask for multiple tenants and privilege levels before an engagement starts. Because APIs change faster than anything else you own, we test them on specification change rather than annually as part of continuous penetration testing. Where source access is granted, whole-codebase review finds the endpoints that are missing the check rather than the ones we happened to sample.
