Home
VAPT Web Application PentestAPI PentestMobile App PentestInfrastructure PentestAI & LLM PentestOT / ICS PentestIoT PentestPenetration TestingAll VAPT services
Red Team Red Team EngagementAdversary SimulationAssumed BreachPurple TeamingSocial Engineering
CompanyResourcesBlogFree Consultation

How to test authorisation in a web application, role by role

Authorisation flaws are found by comparing what two accounts can do, not by scanning one. This is the matrix, the replay method and the evidence a finding needs.

Joel Aviad OssiJoel Aviad OssiRed Team Lead, RedTeam Security
6 min read
A grid of blank square cards pinned in rows and columns on a board, some cells left open and others closed with a small latch, two keys of different sizes lying across separate rows.

Key takeaways

  • Authorisation cannot be judged from a single request. Every test is a comparison between two identities, so an engagement given one set of credentials has not covered it.
  • The unit of work is the denied cell in a role-by-function matrix, and the matrix is built before the application is touched.
  • Horizontal access, one account reading another account's records, is the case tools miss most reliably, because nothing in the request is malformed.
  • Responses have to be read on the server's terms: a 403 that still carries the record in its body is a failure, not a pass.
  • A finding is fixable when it names the role, the request, both sessions and the server-side layer where the check belongs.

Start from the matrix, not from the application

Authorisation is the one web vulnerability class that cannot be judged from a single request. A scanner reads a response and decides whether it looks wrong. An authorisation test needs two identities and a comparison between them, which is why the work starts away from the screen, in a table that records which role may perform which function.

Build that table from the roles the application implements, not from the organisation chart. Rows are functions, columns are roles, and each cell holds the behaviour the application is supposed to produce. Filling it in is a conversation with whoever owns the product, and it tends to surface disagreement before any testing happens: two people who both own the application will answer differently about what a manager may export. That disagreement is worth having in the first week rather than in the report.

The matrix also sets the shape of a web application pentest, because the cell count, not the page count, is what the days go on. Three roles across twenty functions is sixty cells, and every denied cell is a test with a defined pass. What the engagement reaches overall, and what sits outside it, is set out in what a web application pentest covers.

An excerpt from an authorisation matrix for a three-role application. The cells hold expected behaviour, agreed before any testing starts.
FunctionAdministratorManagerStandard user
View an invoiceAny invoiceOwn team onlyOwn records only
Export the customer listAllowedDeniedDenied
Change another user's email addressAllowedDeniedDenied
Approve a refund above the limitAllowedDeniedDenied

The four shapes an access control failure takes

Access control failures read alike in a report and are found in different ways. Naming the four shapes keeps a test from collapsing into the easiest one, which is confirming that a low-privileged user cannot open the administration menu. OWASP groups all of them under broken access control, the category it ranks first.

The horizontal case is the one tools miss most reliably, because nothing in the request is malformed. A valid user asks for a valid record with a valid session, and the only thing wrong is ownership. It carries its own identifier, CWE-639, and finding it needs a second account holding data of its own. That is the practical reason a test account list with one entry per role, rather than one entry in total, is asked for at kick-off.

The four failure shapes, what the tester changes in the request, and what a pass looks like.
Failure shapeWhat the tester changesWhat a pass looks like
Vertical escalationThe session is swapped for a lower role, request otherwise identicalThe server refuses the action for the lower role
Horizontal accessThe object identifier is changed to one owned by another accountThe record is not returned, and the refusal matches the one given for a record that does not exist
Forced browsingAn administrative function is requested directly, without going through a menuThe refusal happens on the server, not by hiding the link
Mass assignmentAn extra field, such as a role or an owner, is added to the request bodyThe field is ignored and the stored value is unchanged

Replay the same request from three identities

The mechanic is the same for every denied cell. Perform the function once as a role that is allowed to, capture the request, then send that same request again with the session of a role that should not be able to, and once more with no session at all. Three responses, one comparison. The OWASP Testing Guide's authorisation testing chapter works through the cases in roughly that order.

Reading the responses is where these tests go wrong. A redirect to the login page is a pass. A 200 that returns the record is a failure. A 403 that still carries the record in its body is also a failure, and so is a 200 whose payload holds the data next to a flag telling the browser to hide it, because the browser is not the thing being tested.

Two practical points. Sessions expire during a long replay run, so every response has to be checked for the login page rather than assumed to be a real answer, or an afternoon of apparent passes means nothing. And identifiers that are random make horizontal testing slower without making it safe: a record nobody can guess is still returned to anyone who asks for it after seeing it once, which is why the test uses a known identifier from the second account rather than a guessed one.

What the finding has to show to be fixable

A finding that says the application has broken access control is not actionable. What a developer needs is the role, the function, the exact request and the response that proves the boundary did not hold, with both sessions in the evidence rather than one. NIST SP 800-115 makes the same point generally: the result of a test is the reproducible procedure, not the tester's conclusion about it.

An example, constructed for illustration rather than taken from any engagement, of the fields that make one reproducible:

The fix location matters as much as the evidence. Almost all of these resolve in server-side code that checks ownership before an object is returned, and naming that layer keeps the change from landing in the front end, where it will be reverted by the next release. The rest of the structure a finding needs is set out in what a penetration test report has to contain.

An example authorisation finding, constructed for illustration, showing the fields that make it reproducible.
FieldContent
TitleA standard user can read invoices belonging to other accounts
Roles usedAccount A, standard user; Account B, standard user in a different organisation
RequestThe invoice detail request captured as Account A, replayed with Account B's session and Account A's invoice identifier
ObservedHTTP 200 with the full invoice, including the billing address
ExpectedThe refusal returned for an invoice that does not exist
Fix locationOwnership check in the invoice service, before the record is serialised

The matrix does not stop at the browser

The same grid applies to everything else that speaks to the same data. A function refused in the web interface is often permitted by the service the interface calls, because the check was written into the page controller rather than the layer beneath it. Test the interface alone and you have an answer about the interface.

So the matrix is worth carrying across engagements rather than rebuilding each time. An API pentest runs the same denied cells directly against the endpoints, with tokens instead of cookies, and a mobile app pentest runs them from a client the user controls completely, where anything the application decides locally can be undone. The rows stay the same. Only the identity mechanism changes.

Role coverage is the first thing a UAE reviewer asks about

Regulated buyers in the UAE tend to read a web application report for coverage before they read it for findings, and role coverage is both the easiest part to check and the easiest to leave out. A report describing one test account for an application with four roles has not tested authorisation, whatever else it contains. Banks and finance companies answer to the Central Bank of the UAE and its rulebook, Dubai government entities and their suppliers to the Dubai Electronic Security Center and the Dubai Cyber Security Standard, Abu Dhabi healthcare providers to ADHICS, and federal entities to the UAE Information Assurance Standard.

The difference between the two emirates is procurement rather than technique. A Dubai supplier is usually asked to show testing against the standard its client is certified to, and a free zone entity in Dubai Silicon Oasis meets the same requirement as a mainland one the moment it sells into a government or banking customer. In Abu Dhabi the healthcare sector asks through ADHICS controls that reach patient records directly, so the horizontal case carries more weight than usual: one clinician reading another clinician's patients is an access control failure and a privacy event at the same time. The matrix is the artefact that answers the coverage question in either emirate, and it belongs in the scope document before it appears in a penetration testing report.

Frequently asked questions

How many test accounts does a web application pentest need?

At least one per role, and two accounts for any role that owns records of its own. The second account is what makes horizontal access testing possible: without a record belonging to someone else, there is nothing to reach across to. Accounts should hold representative data rather than being empty, because an empty account produces the same response whether the check works or not.

Can a scanner find broken access control?

It finds part of it. Scanners are good at unauthenticated access to functions that should require a session, and at pages left reachable by direct URL. They are unreliable on horizontal access and on business rules, because both need to know which account should own which record, and that knowledge lives in the role matrix rather than in the traffic.

What is the difference between vertical and horizontal access control failures?

Vertical means a lower-privileged role performing a higher-privileged function, such as a standard user running an administrative export. Horizontal means one account at the same privilege level reaching another account's data, such as reading a second customer's invoice. Vertical failures are usually caught first because the function looks privileged; horizontal failures need a second account with data of its own.

Should authorisation be retested after the fix?

Yes, and against the same matrix cells rather than only the reported request. Ownership checks are often added to a single endpoint while neighbouring endpoints on the same object keep the original behaviour. Retesting the row instead of the finding is what shows whether the fix was applied at the service layer or patched in one place.

The engagements this applies to

API Pentest

Object and function level authorisation, tokens, rate limits

Joel Aviad Ossi, Red Team Lead at RedTeam Security
Joel Aviad OssiRed Team Lead, RedTeam Security

Joel Aviad Ossi is Red Team Lead at RedTeam Security, the Dubai-licensed trading brand of WebSec FZCO. He scopes and runs objective-based engagements across the UAE.

Let's talk about your security

Tell us the objective you want tested. We will come back with a scope, a timeline and a quote, under NDA from the first conversation.

Email [email protected]
IFZA Business Park, Building A2
Nadd Hessa, Dubai Silicon Oasis
Dubai, United Arab Emirates