JWT Security
header.payload.signature, each Base64Url. Header and
payload are encoded, not encrypted, so the client reads them fine.
All the security is in the signature. Break the signature check and
you forge any claim.
alg values that matter: none (no
signature), HS256 (HMAC, shared secret),
RS256 (private key signs, public key verifies).
Tools
- jwt.io: decode, and re-sign once you have the secret/key.
- xjwt.io: tamper and forge,
including
noneand confusion attacks. Use when jwt.io rejects a malformed token. - CyberChef (URL-safe Base64) for hand-editing bytes,
hashcat -m 16500for cracking HS secrets.
First thing on any token: decode the payload. Note
alg, hunt for privilege claims (admin,
role), and for leaked secrets.
Sensitive data in claims
Payload is public. If devs treat it like a server-side session you get password hashes, internal hostnames, keys. Just decode and read.
Signature not verified
Server never checks the signature. Test by stripping it: send
header.payload. (trailing dot, empty third part). Still
accepted means it is ignored, so flip admin to
1. Rare on public APIs, common server-to-server.
alg: none
Set header alg to none and drop the
signature. If the server does not pin the algorithm, verification
returns true. Try None/nOnE against naive
filters. Cause: reading alg from the untrusted header
straight into the verifier.
Weak HS secret
HS256 is only as strong as its secret.
# wordlist: github.com/wallarm/jwt-secrets
echo '<token>' > jwt.txt
hashcat -m 16500 -a 0 jwt.txt jwt.secrets.listCrack it, then re-sign a tampered token with that secret.
Algorithm confusion (RS256 to HS256)
App signs RS256 and verifies with the public key, which you can
usually get (JWKS endpoint, published, sometimes in the token).
Change alg to HS256 and sign with HMAC
using the public key as the secret. The server HMACs with that same
public key and it matches, so you forged a token without the private
key. Bites older/misconfigured libs that accept both algorithm
families.
Missing / long exp
JWTs are not revocable by default. No exp (or a huge
one) means a stolen or forged token lives forever. Libraries only
check exp when present. Decode and look.
Cross-service relay (aud not enforced)
One issuer, many apps. The aud claim says which app
a token is for, but each app has to enforce it. If app A skips the
check, replay a token minted for app B (where you are admin) against
app A and the admin claim gets honoured. Valid
signature the whole way.
kid / header injection
kid tells the server which key to verify with.
Unsafe use makes it a sink:
- Path traversal:
kidas a filename (../../dev/null, or a file you control/predict) so you verify against a key you know. Seen pointing at a.keyfile on disk. - SQLi or command injection when
kidhits a query/shell. jku/x5u: header URL to a JWKS you host, server trusts your key.
Checklist
Defence
Pin the accepted alg server-side, never trust the
header. Reject none. Never mix symmetric and asymmetric
in one verify path. Long random secret. No sensitive data in claims.
Always set and check exp, enforce aud.
Treat kid/jku/x5u as
untrusted.