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 Source header 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.

MULTI-TENANT SAAS · SEGMENTED BY SUBDOMAIN b-one.target.com b-two.target.com api.target.comone shared API · all tenants Source: B-One Source: B-Two tenant routing trusts the client-set Source header JWT identifies the user by email (username) - no tenant_id / aud / scope ✕ nothing cryptographically binds a token to its tenant
Fig. 01 - the architecture. Two subdomains share one API; tenants are told apart only by a client-supplied 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.

THE TAKEOVER register victim emailon b-two → get JWT replay to api.target.comAuthorization: JWT(b-two)Source: B-One b-one accounttaken over ↳ then repeat with an admin email (found via OSINT) admin emailregister on b-two full admin across the platformall users · reset creds · logs · billing
Fig. 02 - the exploit. A JWT minted on one tenant is replayed against another by flipping the 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

  1. JWTs validated by email. Tokens were trusted on the username field instead of a stable sub/ID. Emails can be duplicated or spoofed across tenants.
  2. No email verification. Anyone could register any address, removing the ownership-proof layer entirely.
  3. No tenant-bound claims. Tokens had no tenant_id, aud, or scope to restrict where they were valid.
  4. A trusted Source header. 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 sub or 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 Source for 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