awesome-everything RU
↑ Back to the climb

Security

JWT pitfalls: code and token reading

Crux Read real JWT verification snippets and a forged token, spot the vulnerability, and pick the highest-leverage fix a security-minded senior makes first.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

JWT bugs hide in the verify call and the claim checks that follow it. Read each snippet the way you would in a security review, find what the attacker exploits, and choose the fix before adding another line of code.

Goal

Practise the review loop you run on every auth path: read the verify call, ask what the attacker controls, and reach for the pin-and-validate fix that closes the trust gap.

Snippet 1 — the forged alg:none token

header:  { "alg": "none", "typ": "JWT" }
payload: { "sub": "victim", "role": "admin" }
signature: (empty)

// server code
const claims = jwt.decode(token);   // decode only, no verify
if (claims.role === "admin") grantAdmin();
Quiz

What does this code grant the attacker, and what is the fix?

Snippet 2 — verifying with the public key

const publicKey = fs.readFileSync("rsa-public.pem");
function verifyToken(token) {
  // no algorithms option passed
  return jwt.verify(token, publicKey);
}
Quiz

An attacker forges a token with alg:HS256 signed using the contents of rsa-public.pem as the HMAC secret. Why does verifyToken accept it?

Snippet 3 — signature checked, claims not

const claims = jwt.verify(token, key, { algorithms: ["RS256"] });
// signature OK — now trust everything
req.user = claims.sub;
req.roles = claims.roles;
// no exp / iss / aud checks beyond the library default
Quiz

The algorithm is pinned and the signature verifies. In a microservice mesh sharing one signing key, what is still wrong?

Snippet 4 — resolving the key from the header

function getKey(header) {
  // header.kid is attacker-controlled
  return fetch(header.jku)
    .then(r => r.json())
    .then(jwks => jwks.keys.find(k => k.kid === header.kid));
}
const claims = jwt.verify(token, await getKey(jwt.decodeHeader(token)));
Quiz

How does an attacker fully control verification here, and what is the fix?

Recap

Every JWT defect is read in the verify call and the checks around it: decode-only code trusts unsigned claims; a missing algorithms allowlist lets the header pick HS256 and turns a public key into an HMAC secret; a verified signature with no aud/iss check is a confused-deputy waiting to happen in a shared-key mesh; and resolving kid/jku from attacker-controlled headers hands key selection to the attacker. The fix is always the same shape — pin the algorithm, bind and allowlist the key, then validate the registered claims against what this service expects.

Continue the climb ↑JWT pitfalls: break and harden an auth flow
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.