Testing Authorisation Properly: The Bugs Automation Misses
Testing authorisation properly: a role, object and verb matrix, two accounts per role, horizontal and vertical checks, and evidence that ends the dispute.
To test authorisation properly, build a matrix of roles, objects and verbs before you start, use at least two accounts per role, test horizontal and vertical access separately, and record which cells you exercised as well as which failed. Broken access control tops the OWASP Top 10 in both the 2021 list and the 2025 revision, and it is the category automation serves worst.
Automated tools can tell you a header is missing. They cannot tell you that a project manager at one company can read another company's purchase orders by changing a number, because they do not know what a company is. That makes authorisation the part of a web application test where a human being earns their fee, and it is worth doing systematically rather than opportunistically. The OWASP Top 10 2025 changes left it exactly where it was.
Build the matrix before you test
Two artefacts, sketched at scoping and finished on day one.
Roles. Every distinct role, including the ones nobody mentions: support staff, read-only auditors, API service accounts, the deactivated user, the invited-but-not-yet-accepted user, the user whose subscription lapsed.
Objects. Every type of thing the application owns, and who is supposed to read, modify, create and delete each one.
The matrix is roles against objects against verbs. You will not test every cell and you should not pretend otherwise. You will test the cells where a wrong answer matters, and at the end you will be able to say precisely which those were, which is more than most reports manage.
Two roles is one boundary. Five roles is ten. If the client adds a role during the engagement, that is a scope change, and it is much easier to say so on day one than in the debrief.
Test the two failures separately
Horizontal: same role, someone else's data
Two accounts at the same privilege level in different tenants or organisations. Take a request from one, replay it with the other's session, and change nothing else. Then hunt the identifier everywhere it hides:
- In the path, and in the query string.
- In a JSON body, including nested objects and arrays.
- In a GraphQL variable, and in a batched query alongside a legitimate one.
- In a filename, an export parameter, a print or PDF route.
- In a WebSocket frame after the connection is established.
- In a header the application put there itself, which is where the interesting ones live.
This is where the serious findings usually are, and it is the class of bug most likely to be reachable by an ordinary customer rather than an attacker.
Vertical: lower role, higher function
Take an administrative action, capture it, and replay it as the least privileged account you hold. Then again with no session at all. Then check whether the endpoint accepts a method it should not, because authorisation applied to POST and forgotten on PUT is a genuinely common pattern rather than a theoretical one.
Where checks go missing
| Place | Why it happens |
|---|---|
| Second and later steps of a flow | Step one is authorised and the rest inherit trust from a session variable set there. |
| Exports and reports | The interface filters by tenant; the CSV export runs its own query, written by someone else, months later. |
| Bulk endpoints | A check written for a single identifier is applied to an array and validates the first element only. |
| Sort, filter and search parameters | Anything reaching a query builder can reach rows it should not, without any injection involved. |
| File and attachment routes | Served from a predictable path, or from a storage bucket that is public with an unguessable name. |
| Cached responses | A shared cache keyed without the tenant returns one customer's data to another, with no vulnerable code anywhere in the request path. |
| Recently shipped features | The pattern established in year one was not applied to the endpoint shipped last month, because the person who established it has left. |
Where the check lives is the finding
An application that enforces authorisation in the interface, in middleware, and in the database survives one of those being wrong. An application enforcing it in exactly one place, in application code, alongside every other concern, will eventually ship an endpoint that forgot, and the only question is which one.
Report the instance you found, then report the reason there will be another one.
When you write it up, say where enforcement happens today and what it would take to move it somewhere a future endpoint cannot bypass. That recommendation is worth considerably more than the identifier you changed, and it is the difference between a finding and advice.
Evidence that survives a dispute
Access-control findings are disputed more often than any other class, and the dispute is almost always the same sentence: "that account did have permission". Capture the pair that ends it, to the standard set out in evidence that survives peer review.
- The full request as sent, including headers.
- Which account the session or token belongs to, and its role.
- The response, with enough of the returned data to prove ownership and no more.
- The same request from the legitimate account, for comparison.
- A note of what you did not do, such as modifying or deleting the object you could reach.
Capture the minimum that proves ownership. One record, with the identifying field visible and the rest cropped, is stronger evidence than a screenshot of a hundred rows and a considerably smaller problem for you to store.
Making the coverage provable
The matrix approach has a second benefit beyond finding bugs: it is the only practical way to answer the question a client asks afterwards, which is not "what did you find" but "what did you check", the question at the heart of proving what you tested when a client asks.
An access-control test that produced no findings is a useful result only if you can say what was exercised. Without that, a clean report is indistinguishable from a shallow one, and the client cannot tell the difference either.
- Record the role pairs you tested horizontally, not just the ones that failed.
- Record the administrative actions you replayed at lower privilege.
- Note the surfaces you could not reach, and why. An untested surface named in the report is far better than one silently omitted.
The value of a negative result is entirely in the specificity of what was tried.
The matrix that actually finds it
Authorisation testing needs at least two accounts at every role, because the interesting question is usually not "can a low-privileged user reach the admin function" but "can one customer reach another customer's object". One account per role cannot answer that.
- Horizontal: same role, another tenant's or user's object. Identifiers in URLs, bodies, headers and hidden fields.
- Vertical: lower role invoking a higher role's function, including functions the interface never shows them.
- Object versus function: a system can enforce that you may call an endpoint while failing to check which record you asked it for. Test both.
- Unauthenticated: the same requests with the session removed entirely, which still succeeds more often than it should.
- Mass assignment: whether a field the interface never sends, such as a role or an owner identifier, is accepted when you send it.
Why automated tools cannot answer this
An automated tool sees a 200 and a body. What it cannot know is whether this user was supposed to receive that body, because that fact lives in the client's business rules and nowhere in the response. Authorisation is a statement about intent, and intent is not observable from traffic.
This is why authorisation findings are among the most valuable things in a report, and also why they are the first thing lost when a test is compressed. If a scope cut has to happen, cutting authorisation coverage is the cut that most changes what the test is worth.
Each phase carries its own checklists, and the web application checklist in the library is mapped to the OWASP WSTG, authorisation included, and suggested on matching phases. Mark it visible to the client and it appears in their portal, which turns "what did you actually check" into something you can point at instead of describing. Checklists import and export as Excel, Word or JSON, so a firm's own authorisation checklist can sit alongside it. See how checklists and phases work.
Common Questions
- How do you test for broken access control?
- Build a matrix of every role, every type of object and every verb before testing, then use at least two accounts per role. Replay requests across tenants with the identifier changed (horizontal), replay higher-role actions as the lowest role and with no session (vertical), try verbs the interface never sends, and send fields it never sends. Record which cells you exercised, not only which failed.
- What is the difference between horizontal and vertical privilege escalation?
- Horizontal escalation is a user reaching another user's or another tenant's data at the same privilege level, usually by changing an identifier. Vertical escalation is a lower role invoking a function reserved for a higher one, often one the interface never shows them. Test them separately, because they fail in different places and horizontal flaws are the ones ordinary customers stumble into.
- Can automated tools find authorisation vulnerabilities?
- Rarely. An automated tool sees a successful response and a body, but it cannot know whether this user was supposed to receive that body, because the rule lives in the client's business logic and nowhere in the traffic. Authorisation is a statement about intent. That is why it is the part of a web application test where human testing earns its fee.
- How many accounts do you need for authorisation testing?
- At least two at every role. The interesting question is usually whether one customer can reach another customer's object, and a single account per role cannot answer it. Ask for accounts in different tenants where the application is multi-tenant, and include the roles nobody mentions: support staff, read-only auditors, service accounts and deactivated users.
Keep Reading
Testing an API When There Is No Specification
Testing an API with no specification: where to find one that is not called one, how to build the route inventory, where to spend the days, and what to report.
Reporting a Critical Finding During a Penetration Test
Found something critical during a penetration test? Confirm enough to be right, call the agreed contact, follow up in writing, and set the threshold beforehand.
Password and MFA Policy That Matches the Guidance, Not the Folklore
A password and MFA policy that matches NCSC and NIST guidance: no composition rules or forced expiry, a length floor, breached-password screening and real MFA.
Setup guide
See the product set up, end to end
The whole setup in 10:01, with chapters you can jump to: registering, your own domain, your own database in any of three places, your own mail server, branding, the first sign-in, updating the database on Supabase Cloud, and moving the database if you change your mind.
- 0:00 · Registering, signing in, and the free trial
- 0:44 · Your name and your firm’s name
- 0:50 · Custom domain
- +6 more