Forging Admin: When a Leaked Secret Becomes a Root Login
A token is only as strong as its secret
A JWT is three base64 chunks: header, payload, signature. The signature is the whole security model. It proves the payload was signed by someone who holds the secret. So if that secret leaks, the model inverts. Now anyone can sign anything, and your server will believe it, because believing a valid signature is its entire job.
This is the step that turns a leak into a login. It is worth understanding exactly how short the path is.
Three ways the secret gets out
- In the bundle. A
NEXT_PUBLIC_orVITE_prefix ships the variable straight to the browser. People do this to signing secrets more often than you would think. Grep your built JavaScript for it before an attacker does. - Weak secret.
HS256with a secret likesecret,changeme, or your company name is brute-forced offline in seconds. There is no network noise for you to notice. - Algorithm confusion. Your tokens use
RS256(a public/private key pair), but a lax verifier also acceptsHS256. The attacker signs a token withHS256using your public key, which is public, as the HMAC secret. A verifier told to "just verify" accepts it.
Where do secrets like this leak from in the first place? Usually the file in The .env File That Ends Companies.
From secret to admin, in one decode
Take a normal session token and look at the payload:
{ "sub": "user_77", "role": "user", "iat": 1756300000, "exp": 1756386400 }Change one field, re-sign with the leaked secret, and you are done:
import jwt # PyJWT
payload = {"sub": "user_77", "role": "admin", "iat": 1756300000, "exp": 1756386400}
forged = jwt.encode(payload, "leaked-secret-here", algorithm="HS256")
print(forged)The server checks the signature, finds it valid because it was signed with the real secret, and reads role: admin. You are now an administrator without ever knowing an admin password, resetting one, or triggering a login alert.
Every session token you have ever issued shares one secret. When that secret leaks, you are not exposed to one forged login, you are exposed to unlimited forged logins as any user, any role, until you rotate. Rotation is the only real remedy.
How to tell if you are exposed
You do not need to wait to be attacked to find out. Three quick checks cover most of the risk:
- Grep your shipped bundle. From your built front-end, search for the secret and for tell-tale prefixes. If anything comes back, the secret is already public and rotation is not optional.
# Run against your production build output, not your source
grep -rIE 'JWT_SECRET|SUPABASE_JWT|NEXT_PUBLIC_.*SECRET|VITE_.*SECRET' dist/ .next/- Decode a real token. Paste one of your own session tokens into any offline base64 decoder and read the header. If
algisHS256and your backend also issuesRS256elsewhere, you have the ingredients for algorithm confusion. - Test the weak-secret case. If the header says
HS256, a wordlist run against the token finishes in seconds on a laptop. If a common word cracks it, so will an attacker's.
None of these touch another user's data. They tell you whether the front door is already unlocked.
Proving it without doing damage
You do not break anything to prove this. You forge a token with elevated claims and hit a read-only privileged endpoint:
GET /api/admin/me HTTP/1.1
Host: app.yourco.com
Authorization: Bearer eyJhbGci...forged-admin-tokenA 200 that returns an admin context in the body is the evidence: request in, response out, nothing mutated. That saved pair is the difference between "theoretically exploitable" and "here is the proof." Once you can forge a session, the natural next target is data an ordinary user should never touch, which is exactly the Supabase RLS Gaps an attacker chains to next.
Shutting it down
| Failure | What the attacker does | Fix |
|---|---|---|
| Secret in the bundle | Reads it from your JS | Never prefix a secret as public |
Weak HS256 secret | Cracks it offline | 32+ random bytes from a CSPRNG |
| Algorithm confusion | Signs with your public key | Pin an algorithms allowlist |
Concretely:
- Rotate the secret now if there is any chance it leaked. Rotation invalidates every forged token along with the real ones.
- Use a long, random secret, never a word. Thirty-two bytes from a real random source, minimum.
- Pin the algorithm. Tell your verifier
algorithms=["RS256"]explicitly and reject everything else. This kills both algorithm confusion and thealg: nonetrick in one line. - Keep signing secrets server-side. If it can reach a browser, it is not a secret.
Notra forges a token against your own endpoints, non-destructively, and shows you the exact request and response if one gets in. No guessing, no "possible." Run a free scorecard, or read more of our field notes on the blog.