Dunicot A cybersecurity consultancy and advisory firm.

Access Control · Case Study · 7 min read

The role that was not a role

A read-only account on a B2B platform could export the data its own screens refused to display. We found it in the first hour of authenticated testing, and the reason it survived two previous tests is worth more than the bug.

The client was a B2B SaaS platform used by operations teams to manage inventory across multiple sites. It had been tested twice before by two different firms, and both reports came back with a handful of medium findings and nothing above them. We were asked for a third opinion because an enterprise buyer had made one a condition of signing.

We were given four accounts across three roles: administrator, manager and a read-only viewer intended for auditors and external accountants. That last role is the one that mattered, and it is the one neither previous test had been given.

What we did first

Before testing anything we built the feature map. Every role, every screen, every action, and for each action the identifier it operates on. This takes a day and it is the part that makes the rest of the engagement productive, because an authorisation gap is a deviation from intent and you cannot spot a deviation until you have written the intent down.

The map showed something immediately. The viewer role had no export button anywhere in its interface, but the manager and administrator roles both did, and all three roles hit the same reporting service.

The finding

We logged in as the viewer, opened the network tab as a manager in a second browser, and took the export request the manager made. Same endpoint, same parameter shape. We replayed it with the viewer session cookie.

It returned the file. Not a subset, not a redacted version, the full export including cost prices, supplier contracts and the contact records for every site the organisation ran. The viewer role could not see any of that on screen. The reporting service had never been told that the viewer role existed, so it applied the only rule it knew, which was that the session was valid.

We then checked whether the same service would accept a site identifier belonging to a different customer. It would.

Why two previous tests missed it

Neither had been given the viewer account. Both reports listed their scope honestly, and in both cases the scope said administrator and standard user. The client had provisioned two accounts because that is what the scoping question asked for, and nobody on either side had asked what other roles existed.

This is the most common reason we find access control issues that previous testers did not. It is rarely a question of skill. It is a question of whether the engagement was given the accounts it needed to compare one role against another, because a single account cannot demonstrate a boundary between two.

What we asked the client to fix

Not the export endpoint. Fixing that one route would have left every other route the reporting service exposed in the same state, and we had already found two more.

The recommendation was to move authorisation out of the route handlers and into the data layer, so that every query for a site record is scoped by the requesting user’s permitted sites before it runs, and a route that forgets to check simply returns nothing. The client shipped it across the service in three weeks. We retested in a clean session and could no longer reach anything from the viewer account that the viewer interface did not show.

The part that generalises

If your application has more than two roles, a test with two accounts cannot tell you whether your authorisation works. It can only tell you that one account behaves as expected.

Before your next engagement, write down every role your product has, including the ones created for customer support, for auditors, for the onboarding team, and for whatever integration you built in a hurry two years ago. Provision one account for each. If a firm says it does not need them, that is useful information about the firm.

In short

Point 1
Provision an account for every role, not for the two the scoping form asked about.
Point 2
Interface visibility is not authorisation. If the API answers, the control does not exist.
Point 3
Fix the pattern rather than the route. One missing check is usually a class, not an incident.
Point 4
A report that lists only medium findings often describes the scope it was given rather than the application.

Want this applied to your stack?

Everything written here comes out of delivered engagements. Describe the platform and the deadline.