Devs sometimes ask why I need two accounts per role. It's a fair question. Extra accounts mean extra provisioning, and on a staging environment with SSO wired into a corporate directory it isn't always a five minute job.
The short answer is that one account finds an IDOR. Two accounts prove it.
Finding is the easy half. IDs are sequential, or a UUID leaks into a response that had no reason to carry it, or an export endpoint accepts an identifier the UI never exposes. You increment, you substitute, you get a 200 back. That part rarely needs more than one session.
Confirming is where one account falls apart.
Say I request object 4471 and the server hands me data. What am I looking at?
- Orphaned test data somebody left in the fixture set
- Something I own through a path I had forgotten about
- A real customer's record
Three very different answers, and from a single session I cannot tell them apart. The response looks identical in all three cases. I can guess from the shape of the data, and a guess is not what anybody is paying for.
The third case matters more than the other two, and not for the reason you would expect. If it is a real customer's record and we are testing production, I have not found a vulnerability. I have accessed data I had no business seeing. Depending on what was in it, that is closer to an incident than a finding, and the right move is to stop, write down exactly what happened and tell the client the same day. That is a bad afternoon for everyone, and it started because the test setup left me no way to tell the difference.
With two accounts at the same privilege level there is no ambiguity. Create the object as user B. Try to reach it as user A.
# user B creates the object, and owns it
$ curl -s -H "Authorization: Bearer $B_TOKEN" \
-X POST https://staging.example.com/api/invoices \
-d '{"amount":100,"ref":"pentest-b-001"}'
{"id":4471,"owner":"user-b","ref":"pentest-b-001"}
# user A, same role, different owner, asks for it anyway
$ curl -si -H "Authorization: Bearer $A_TOKEN" \
https://staging.example.com/api/invoices/4471
HTTP/1.1 200 OK
{"id":4471,"owner":"user-b","ref":"pentest-b-001","amount":100}The reference string is mine. The owner field says user-b. The session says user A. There is nothing left to interpret.
The sentence in the report changes with it:
Before: the endpoint returned data for an identifier I did not recognize.
After: user A retrieved user B's invoice.
The first one is a lead. The second is a finding, with a severity somebody can defend, reproduction steps somebody can run and a fix somebody can verify. Nobody has to take my word for who owned object 4471, because I created it thirty seconds earlier.
Multi-tenant applications are the same idea one level up. Two accounts in two different organizations, on top of the two accounts per role inside one of them. Per-user isolation and per-tenant isolation are usually separate code paths, often written years apart by different people, and an application can get one right while the other was never wired up at all. If the scope mentions organizations, workspaces, tenants or institutions, that boundary is where the interesting failures live, and crossing it needs accounts on both sides.
It does not have to be literally two accounts. Seeded records with known owners work just as well. If the client hands me a fixture set and tells me which user owns what, I get the same certainty. Two accounts is usually just the cheapest way to get there, because nobody has to write anything.
So when I ask, this is the list:
- Two accounts per role in scope, at the same privilege level
- If the application is multi-tenant, accounts split across two organizations
- Staging with synthetic data, not production
- Owners I can attribute, so I know which side created what
None of that costs the client anything. It changes what the test is able to prove.
If you are on the buying side and this never came up in your scoping call, it is worth asking about. It is a cheap proxy for whether the tester intends to prove impact or just report anomalies, and it shows up in the document you eventually hand your auditor. That document reads either "the endpoint behaved unexpectedly" or "authorization is missing on this object, here is the proof, here is the fix". Only one of those closes a finding.
