From Establishment IDOR to Account Takeover: Changing One Number Was Enough

Severity: High
Bounty: ~$1145
Program: Private Bug Bounty
Platform: Bugbounty.sa
Some account takeovers start with stolen passwords, leaked tokens, or complicated OAuth chains.
This one started with changing an establishment number.
The target was an online business-services platform where authenticated users could access and manage services on behalf of establishments associated with their accounts.
During the authentication flow, the application sent an establishment identifier to:
POST /auth/login
The backend trusted that identifier without properly verifying that the establishment actually belonged to the authenticated user.
So I replaced my establishment ID with another valid one.
The request succeeded.
And I was logged into the other establishment.
No password for the target establishment. No stolen session. No token leak.
Just one changed number.
A Little Background
The target was a government-backed platform used by businesses to access establishment-specific services and workflows.
A single user could interact with the platform in the context of an establishment they were authorized to represent.
That distinction is important.
Authentication answers:
Who is this user?
But after authentication, the platform also needed to answer:
Which establishment is this user allowed to represent?
Those are two separate security decisions.
The application handled the first one.
The second one was where things went wrong.
Looking at the Login Flow
While reviewing the authentication process, I noticed a request sent when entering the establishment context:
POST /auth/login HTTP/1.1
Host: [target].com
Content-Type: application/json
Origin: https://[target].com
Referer: https://[target].com/auth/login
The body contained something similar to:
{
"number": "9-4827316",
"type": "establishment",
"page": "",
"_token": "[CSRF_TOKEN]"
}
The interesting field was:
number
This represented the establishment the session wanted to access.
At first, there was nothing inherently wrong with sending an establishment identifier from the client.
Applications often need to know which organization, tenant, company, or workspace a user wants to enter.
But that value must be treated as a selector, not proof of authorization.
The server still needs to verify:
Does the authenticated user actually belong to this establishment?
So I tested exactly that.
Changing the Establishment Number
I started with an establishment legitimately associated with my test account.
For example:
{
"number": "9-4827316",
"type": "establishment",
"page": "",
"_token": "[CSRF_TOKEN]"
}
I intercepted the request and changed only the establishment number.
For example:
{
"number": "9-7319042",
"type": "establishment",
"page": "",
"_token": "[CSRF_TOKEN]"
}
Everything else remained the same.
Same authenticated user.
Same session.
Same CSRF token.
Different establishment.
I sent the request.
The backend accepted it.
I Was Now Inside the Other Establishment
This was not simply an information leak where the API returned the name of another establishment.
The application actually established the session in the context of the supplied establishment.
The result was effectively:
My authenticated account
↓
POST /auth/login
↓
number = another establishment
↓
Backend accepts number
↓
Session enters target establishment
The authorization boundary between establishments had been bypassed.
Instead of determining the establishment from my authorized relationships, the server trusted the establishment selected in the request.
That turned a single client-controlled identifier into an account takeover primitive.
Why This Was an Authentication Issue and an IDOR
The bug sat at an interesting boundary between authentication and authorization.
At the object level, the vulnerability looked like an IDOR:
Establishment A
↓
Change identifier
↓
Establishment B
But the vulnerable endpoint was part of the authentication flow.
The attacker was not merely retrieving an unauthorized object.
The supplied identifier determined which establishment the attacker became authenticated as.
Conceptually, the vulnerable logic behaved something like:
$establishment = Establishment::findByNumber(
$request->number
);
loginToEstablishment($establishment);
The missing step was something equivalent to:
if (!$currentUser->canAccess($establishment)) {
abort(403);
}
The application authenticated the user, accepted the establishment number from that user, and then trusted it when establishing the business context.
The Trust Boundary Was Backwards
The request contained:
{
"number": "9-7319042",
"type": "establishment"
}
The server treated this almost like:
"I want to log into establishment 9-7319042"
and responded:
"Okay."
What it should have done was:
User requests establishment 9-7319042
↓
Load authenticated user's establishments
↓
Is 9-7319042 associated with this user?
↙ ↘
Yes No
↓ ↓
Continue 403
The establishment number should have selected from resources the user was already authorized to access.
Instead, it effectively selected the authorization context itself.
That is a dangerous distinction.
Why Guessing Wasn't the Vulnerability
The vulnerability was not that establishment numbers had a recognizable format.
The example identifiers in this writeup are fabricated, but they looked conceptually like:
9-4827316
9-7319042
9-2648195
Even if those identifiers had been long random UUIDs, the authorization flaw would still exist.
Knowing another establishment's identifier should never be equivalent to having permission to access it.
The actual issue was:
Possession of establishment ID
=
Authorization to establishment
An identifier identifies an object.
It should not authenticate its owner.
From IDOR to Establishment Account Takeover
This distinction is what made the impact significantly different from a normal cross-tenant data exposure.
A traditional IDOR might allow something like:
GET /establishment/9-7319042
and return unauthorized information.
Here, the affected request was:
POST /auth/login
Once the backend accepted the manipulated establishment number, the attacker entered the target establishment's authenticated context.
The chain was:
Authenticate normally
↓
Intercept establishment login
↓
Replace number
↓
Submit another establishment ID
↓
Backend fails ownership validation
↓
Authenticated as target establishment
At that point, the issue was no longer about viewing a single unauthorized resource.
It was establishment-level account takeover.
What an Attacker Could Access
The platform provided business-specific functionality after entering an establishment.
That meant successfully switching into another establishment could expose whatever functionality was available to that establishment's authenticated session.
Depending on the target account, this could include sensitive business information and establishment-specific services and workflows.
The important part of the report was that I did not need to individually bypass authorization on every feature.
The authentication flow itself placed my session inside the unauthorized establishment.
Once that happened, downstream functionality treated the session as belonging to the selected establishment.
The trust decision had already been made incorrectly.
A Useful Way to Think About Multi-Tenant Authentication
Applications with organizations, companies, workspaces, establishments, or tenants commonly have two layers:
Layer 1:
Authenticate the person
followed by:
Layer 2:
Authorize the tenant
For example:
User
↓
Login
↓
Choose organization
↓
Verify membership
↓
Organization session
A secure application cannot skip the membership check.
Otherwise:
Choose organization
quietly becomes:
Choose who you want to be.
That was essentially what happened here.
Reporting the Finding
I reported the issue with a video PoC showing the complete authentication flow and the establishment-number manipulation.
The vulnerability was reproduced and accepted as High severity.
The bounty was approximately $1,145.
What I liked about this finding was how little was required to demonstrate the impact.
There was no complicated payload.
No race condition.
No token forgery.
The important request was essentially:
{
"number": "[TARGET_ESTABLISHMENT]",
"type": "establishment"
}
The security impact came entirely from what the backend failed to verify.
The Fix
When I later retested the application, the original vulnerable endpoint no longer accepted the same POST flow.
The authentication process had been moved to a new implementation with stronger validation.
I could no longer reproduce the establishment-switching behavior.
The fix was confirmed.
The important security requirement was straightforward:
Authenticated user
↓
Requests establishment X
↓
Server resolves user's authorized establishments
↓
Verify membership / representation rights
↓
Create establishment context
The establishment ID itself should never determine authorization.
Hunter Takeaways
Pay Attention to the Step After Login
Some of the best authorization bugs appear after the user has already authenticated.
Look for flows such as:
Choose company
Choose organization
Choose tenant
Choose workspace
Choose branch
Choose establishment
Then inspect what the browser sends when switching context.
If you see:
{
"organization_id": "..."
}
or:
{
"tenant": "..."
}
or:
{
"number": "..."
}
ask one question:
What proves that my account is allowed to select this value?
Authentication Does Not Automatically Mean Authorization
A valid session only proves who the user is.
It does not prove that the user should have access to every object they can reference.
This is especially important in multi-tenant applications.
The correct model is:
Valid user
+
Valid establishment
+
Valid relationship between them
=
Authorized establishment session
Missing the third check breaks the entire boundary.
Test Tenant Selection as Aggressively as Normal IDORs
Bug bounty hunters spend a lot of time changing IDs in endpoints like:
/api/users/123
/api/orders/456
/api/documents/789
Do the same thing during authentication and tenant selection.
Fields such as:
company_id
organization_id
tenant_id
account_id
establishment_id
number
workspace_id
can be much more valuable than an ordinary object ID.
If one of those values controls the authorization context for everything that follows, a single missing check can compromise an entire account.
Don't Stop at "I Can Change the ID"
The important question is what changes after the identifier is accepted.
In this case:
Change establishment ID
was interesting.
But:
Change establishment ID
↓
Become authenticated as that establishment
was the actual vulnerability.
Always follow the state transition.
Check the resulting session, accessible pages, API responses, permissions, and tenant context.
That is where a basic IDOR can turn into an account takeover.
Identifiers Are Not Secrets
An establishment number, company ID, username, email address, or account number may be public, predictable, leaked elsewhere, or legitimately known by third parties.
Security should never depend on keeping an identifier secret.
A good rule is:
Identifier = which object?
Authorization = may this user access it?
Those should always remain separate decisions.
Final Thoughts
This was one of those bugs where the exploit was almost disappointingly simple.
The application asked which establishment I wanted to enter.
I gave it another establishment's number.
And it trusted me.
The entire chain was:
Normal authenticated account
↓
Intercept /auth/login
↓
Change establishment number
↓
Submit request
↓
No ownership validation
↓
Session enters another establishment
↓
Establishment account takeover
One field:
number
One changed value.
One missing authorization check.
And an authentication flow that allowed one business user to become another establishment.
Sometimes account takeover does not require stealing credentials.
Sometimes the application simply asks you which account you would like to be.





