← Attack pathsa jwt with alg none tricked the server into admin access

a jwt with alg none tricked the server into admin access

$jwt.decode(tok, key, options={'verify_signature': False})

the trick that shouldn't work but does

here's the setup. a jwt has three parts, a header, a payload, and a signature, all separated by dots. the header says which algorithm signed it. the payload holds claims like "user_id" or "role". the signature is supposed to prove nobody messed with the first two parts.

the attack in this reel is stupid simple once you see it. take a valid token, decode the header, change "alg" to "none", change the payload role from "user" to "admin", then delete the signature entirely. if the server just reads the header and payload without actually verifying a signature, it says welcome, admin. no password cracking, no exploit chain, just editing json and asking nicely.

breaking down the vulnerable code

jwt.decode(tok, key, options={'verify_signature': False})

this one line is the entire vulnerability. that options={'verify_signature': False} flag tells the jwt library "don't bother checking if this was actually signed by us." it just base64-decodes the payload and hands it back as trusted data.

developers add this during debugging all the time. you're testing something, the signature check is annoying, you disable it "just for now," and then it ships to production because nobody remembered to flip it back. the token becomes a trust-me-bro note instead of a cryptographic proof.

the "alg none" part makes this worse because it's an actual spec-legal value. the jwt standard allows an algorithm of none for cases where you don't need integrity checking. some libraries will honor that header value even when verification is technically "on," because they trust the token to tell them how it was signed. that's like asking a suspect to grade their own polygraph.

the fix, line by line

claims = jwt.decode(tok, key, algorithms=['HS256'])
user = User.objects.get(id=claims['sub'])
if not user.is_admin: abort(403)

first line: algorithms=['HS256'] pins the exact algorithm your server expects. this isn't optional decoration, it's the whole fix. the library will now reject any token that claims to use "none" or any other algorithm you didn't explicitly allow. the server decides how tokens get verified, not the token.

second line: pull the user record from your own database using the subject claim. the token is only used to say "here's who this claims to be," nothing more.

third line: check the admin flag that lives in your database, not in the token payload. this is the part people skip. if "role" or "is_admin" is sitting inside the jwt claims, anyone who can edit that json can promote themselves, signature or not, because a lot of implementations only verify the signature and never re-check the sensitive fields against a source of truth.

why this matters even if you "already verify signatures"

a token proves who you are. it should never be the thing that decides what you're allowed to do. permissions and roles need to come from a lookup your server controls, checked on every privileged action, not baked into a blob the client can see and, in the wrong setup, tamper with.

this also applies beyond jwts. any time your app trusts a value because "it was in the signed thing," double check that the signature is actually mandatory, that the algorithm is pinned, and that sensitive fields get revalidated against the database instead of just re-read from the token.

the takeaway

if you run anything that issues jwts, go look at your decode call right now. if you see verify_signature set to false anywhere outside of a local test file, that's an open door. pin your algorithm explicitly, never let the token's own header decide how it gets checked, and treat role or permission fields inside the token as decoration, not truth. the database is the source of truth for who's an admin. the token just gets you in the room, it doesn't get to hand out keys.

watch the reel ↗
the weekly drop

one command a week that makes you harder to hack.

a single tool, explained in plain english, every week. straight to your inbox.

no spam. one email a week. unsubscribe anytime.