
From Anonymous to TenantAdmin: Chaining a Firebase Custom-Claims Authorization Bypass into Full Tenant Takeover
Every so often, a bug isn’t one bug. It’s three boring ones standing on each other’s shoulders, wearing a trench coat. Individually, none of these would make a triager’s heart race. Chained together, they let an anonymous person on the internet become a full administrator of any tenant on the platform with no credentials, no phishing, no clicks.
The platform’s entire authorization model hangs on Firebase custom claims fields like admin s that ride inside the Bearer ID token. The NestJS API trusts those claims to decide who you are. The problem: the endpoint that sets those claims wasn’t actually guarded. So you could mint your own token, then tell the server to make you an admin, and it said yes.
The Mental Model: Who Decides You’re an Admin?
In a Firebase-custom-claims design, the ID token is the source of truth for authorization. When the server sees a claim like admin: “TenantAdmin”, it grants admin powers. That’s fine as long as the only way to get that claim is through a tightly guarded, admin-only operation.
The whole security of the model collapses to a single question: can a user influence their own claims? If yes, the token stops being proof of authorization and becomes a self-service admin button. That’s exactly what happened here.
The Chain, End to End

Figure: Three independent weaknesses compose into a single unauthenticated path from anonymous visitor to Tenant Admin.
Step 1 – The Firebase API key is sitting in the JS bundle
First stop, the front end. Modern SPAs ship their config to the browser, and this one was no exception; the public Firebase Web API key was embedded in clear text inside a served JavaScript bundle, grabbable by anyone who views the source.
curl -s https://<redacted>/assets/index-<hash>.js | grep -oP 'AIza[0-9A-Za-z_-]{35}'
A quick note, because this part is widely misunderstood: a Firebase web API key being public is by design; on its own, it’s not the vulnerability. It becomes dangerous only when the operations it unlocks (here, open self-registration) aren’t locked down.
Step 2 – Tenant IDs are enumerable, unauthenticated
Next, I needed a tenant to target. An endpoint along the lines of /api/…/get-tenant-details/{id} happily answered unauthenticated requests and, given a valid 9-digit numeric ID, returned the tenant’s details, including the Google Identity tenantId needed for the next step.

Two design choices made this exploitable: the identifier was a sequential numeric value (so the keyspace is small and walkable), and there was no rate limiting. A trivial script enumerates valid tenants and harvests their tenantId values at will.
Step 3 – Self-register, then self-promote
Now the two halves come together. With the public API key and a real tenant ID, Then called Firebase’s accounts:signUp endpoint directly to create an attacker-controlled account inside the target tenant. No email verification was enforced, so the account was immediately usable, and the response handed back a valid idToken.
curl -s 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<REDACTED_KEY>' \ -H 'Content-Type: application/json' \ -d '{"email":"<redacted>","password":"<redacted>","returnSecureToken":true,"tenantId":"<redacted>"}'
Then the punchline. The role-granting route POST /api/users/role/{uid} had no server-side authorization guard. Then we sent our own freshly-issued token as the Bearer and asked the server to make my own UID an admin. It complied.
curl -X POST https://api.redacted/api/users/role/<attacker_uid> \ -H "Authorization: Bearer <idToken>" \ -H "Content-Type: application/json" \ -d '{"role":"admin"}'
Log out, log back in, and the new ID token carries the admin claim. Anonymous to TenantAdmin.
The Root Cause (Say It With Me)
Strip away the Firebase specifics, and the lesson is old as the web: the server trusted the client to tell it who was allowed to do something privileged. Custom claims are a fine authorization mechanism, but the operation that grants them is the crown jewel, and it must be guarded like one. A self-directed role change should be structurally impossible.
Remediation
- Guard the role endpoint. Enforce server-side authorization on POST /api/users/role/{id} and every privileged user-management route. A user must never be able to elevate their own account and reject self-targeted role changes outright.
- Kill open self-registration. Disable accounts:signUp for production tenants; provision users through an authenticated invite/approval flow. Firebase blocking functions can enforce this at the identity layer.
- De-enumerate tenants. Require auth on the tenant-details endpoint, replace sequential numeric IDs with non-guessable UUIDs, and add rate limiting to blunt automated walking.
Rotate and restrict the key. Rotate the exposed web API key and lock the replacement down with Firebase App Check and Google Cloud API key restrictions, scoped to the minimum operations.
