notra·
2026-08-27·4 min read·Notra·Reviewed 2026-09-09

Forging Admin: When a Leaked Secret Becomes a Root Login

A token is only as strong as its secret

A JWT is three base64url chunks: header, payload, signature. The signature protects the token from tampering, but authorization also depends on issuer, audience, expiry, and the application's authorization checks. If a signing secret leaks and an application trusts attacker-controlled claims such as role, an attacker may mint tokens the application accepts.

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

  1. In the bundle. A NEXT_PUBLIC_ or VITE_ 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.
  2. Weak secret. HS256 with a predictable word or company name can be tested offline without network noise; recovery speed depends on the secret's entropy, wordlist, implementation, and hardware.
  3. Algorithm confusion. Your tokens use RS256 (a public/private key pair), but a lax verifier also accepts HS256. The attacker signs a token with HS256 using 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)
Ten lines of Python between a leaked secret and an admin session.

In an application that authorizes directly from the role claim, the server may check the signature, read role: admin, and grant administrator access without an admin password. Robust applications also validate issuer, audience, expiry, and server-side authorization state.

Warning

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 alg is HS256 and your backend also issues RS256 elsewhere, you have the ingredients for algorithm confusion.
  • Test the weak-secret case. If the header says HS256, a wordlist can be tested offline. If a common word cracks it, treat the signing key as compromised.

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-token

A 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

FailureWhat the attacker doesFix
Secret in the bundleReads it from your JSNever prefix a secret as public
Weak HS256 secretCracks it offline32+ random bytes from a CSPRNG
Algorithm confusionSigns with your public keyPin 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 the alg: none trick in one line.
  • Keep signing secrets server-side. If it can reach a browser, it is not a secret.

The IETF JWT best-current-practices guidance requires an explicit algorithm allow-list; PyJWT documents 32-byte minimum keys for HS256 in its usage guide. Notra forges a token against your own endpoints, non-destructively, and shows the exact request and response if one gets in. Run a free scorecard, or read more of our field notes on the blog.

ShareX LinkedIn

Related

Want this run against your site?