A red-team engagement that began by registering the same email twice and ended in full admin control of a multi-tenant SaaS. The platform assumed three things were safe, and none of them were:
- email as the identifier inside JWTs,
- a single shared API for every tenant,
- a client-supplied
Sourceheader to tell tenants apart.
Together they added up to a quiet but critical architectural flaw. Here's exactly how it was exploited, and how to design it out.
1. The architecture
The product was multi-tenant, split by subdomain:
b-one.target.com
b-two.target.com
Both routed to one central API:
api.target.com
A custom header separated tenants:
Source: B-One
There was no cryptographic binding between a token and its tenant, and no server-side check that a token issued for B-Two wasn't being used on B-One. That was the Achilles' heel.
Source header, and JWTs are keyed on email - so a token carries no proof of which tenant it belongs to.2. Discovery: duplicate emails, duplicate tokens
I registered the same email on both subdomains.
On B-One:
POST /users/sign-up HTTP/1.1
Host: api.target.com
Source: B-One
{ "email": "victim@example.com", "password": "P@ssWord123" }
On B-Two:
POST /users/sign-up HTTP/1.1
Host: api.target.com
Source: B-Two
{ "email": "victim@example.com", "password": "P@ssWord123" }
Both succeeded. Each returned a different JWT, but both carried the same username (the email):
{
"iat": 1704719426,
"exp": 1704723026,
"username": "victim@example.com",
"id": "960912"
}
So the app identified users by username (email), not by sub or a unique ID. JWT trust rested entirely on an email string.
3. Exploitation: cross-tenant identity injection
With a JWT minted on B-Two, I called B-One:
GET /user/me HTTP/1.1
Host: api.target.com
Authorization: JWT <B-Two token>
Source: B-One
The API returned the B-One user's profile for the matching email. The token wasn't scoped to a tenant, and the backend trusted the Source header as-is. Isolated per-tenant access became cross-tenant access keyed on a reused email.
Source header; trusted by email alone, it impersonates the matching user - then the same move with an admin's email yields full control.4. Escalation: from a user to the admin dashboard
After impersonating a normal user, I escalated:
- found public admin emails for the platform via Google, GitHub, and WHOIS,
- repeated the same move: register the admin email on B-Two, get a JWT, use it on B-One.
The result was full admin across the platform: viewing every account, resetting credentials, reading internal logs and configuration, and reaching client billing and support data.
5. Why it happened
- JWTs validated by email. Tokens were trusted on the
usernamefield instead of a stablesub/ID. Emails can be duplicated or spoofed across tenants. - No email verification. Anyone could register any address, removing the ownership-proof layer entirely.
- No tenant-bound claims. Tokens had no
tenant_id,aud, or scope to restrict where they were valid. - A trusted
Sourceheader. Tenant separation rode on a client-controlled header with no cryptographic enforcement, a trivial bypass.
6. How to fix it
Identity & claims
- Validate tokens by
subor a unique ID, never by email or username. - Enforce email verification before a session is active.
- Put tenant context inside the JWT (
tenant_id,aud, or scope).
Token management
- Sign JWTs with tenant-specific keys, or bind the tenant ID into HMAC validation.
- Rotate keys regularly and watch for anomalies.
Access control
- Never trust client-supplied headers like
Sourcefor security decisions. - Match the token's tenant against the backend context on every request.
- Log and alert on cross-tenant token reuse.
7. Tools
- Burp Suite: edit tokens and spoof headers
- Postman: automate token replay across headers
- jwt.io: decode and inspect claims
- Browser dev tools: live header manipulation
- OSINT: find real user / admin emails
8. FAQ
Could this happen with Firebase / Auth0? Yes, if the platform trusts email claims and lacks tenant-bound scopes or keys, the same class of bug applies.
Should I put email in a JWT? Only as a reference field, never for access control or session validation. Use sub or a UUID.
Can I trust custom headers? Not unless you cryptographically verify them. Anyone with curl / Postman / Burp can set them.
Best practice for multi-tenant tokens? Separate keys, tenant claims enforced in access checks, and backend logic that maps every token to the right tenant.
References
- RFC 7519 (JSON Web Token): https://datatracker.ietf.org/doc/html/rfc7519