Conditions
The application supported personal and organization-level 2FA. Users could belong to several organizations, and the next login reopened the organization they had last used. An organization with mandatory 2FA also required the member to enable account-level 2FA.
Authentication flow
After valid email and password credentials, the pre-authentication response exposed a temporary JWT-like value in a 2fa field. It also listed organization IDs and whether each organization required 2FA.
The temporary value was not a normal API token; direct requests returned 403. It was intended to be submitted with the six-digit authenticator code:
{
"2fa": "<TEMPORARY_TOKEN>",
"code": "<SIX_DIGIT_CODE>"
}
The organization-switch edge case
If a user’s last active organization later enabled organization-level 2FA, the next login offered two choices: set up 2FA or switch to an organization that did not require it. The switch request used the organization ID and the same temporary value:
POST /api/org/<ORG_ID> HTTP/1.1
Host: example.com
{
"2fa": "<TEMPORARY_TOKEN>"
}
The bypass
- Submit the victim’s valid email and password.
- Read the temporary
2favalue and the ID of an organization without mandatory 2FA from the response. - Call the organization-switch endpoint with those values.
The endpoint completed the login and switched organizations without requiring the victim’s account-level authenticator code.
Root cause
Two pieces that looked harmless in isolation became an authentication bypass when combined: sensitive pre-authentication state was disclosed, and the organization-switch endpoint treated that state as sufficient to establish a session. Every branch that finishes login must enforce the account’s strongest authentication requirement.