In Java, you'd deserialize a signed object with the verification method fixed
in the calling code — the bytes on the wire never get a vote in how they're checked. A JWT
is different in a way that catches Java engineers off guard: the token's own header carries
alg, and a verifier that reads it and dispatches on it is letting the
attacker's forgery choose how it gets checked. Every attack on this page is that one
design mistake wearing a different costume.
A compact JWT is three base64url segments — header.payload.signature — joined
by dots. Every token below is a real token: built the same way sec-jwt's
checks build theirs, with a real HMAC-SHA256 signature where one applies. Pick a scenario and
the scene decodes it and runs it past two verifiers: the safe one (checks the located
key's own kind before ever using it, checks exp/nbf,
rejects alg=none unconditionally) and a naive one (trusts whatever the
header claims). They agree on an honest token. They disagree on every attack — and the naive
one is not a strawman, it is the shape of the real CVEs this class of bug has produced.
"Read alg from the header, then verify with it" is the first working version
almost everyone writes, because it is the first version that makes the happy path pass. The
fix is not a smarter parser — it's moving the decision out of the token's hands: the
caller names the algorithms it will accept (allowed_algs), alg=none
is rejected unconditionally, and the key you already hold for a given kid
carries its own kind ("hmac" or "rsa") that the verifier checks before it will use
that key at all — so a public key handed back as an HMAC secret is caught by the key's own
record, not by hoping the header is honest.
alg is attacker-controlled input, and an allow-list the caller sets is the only
algorithm choice that is trustworthy. Reject alg=none unconditionally, not "unless
it's in the allow-list". Every key you hold has a kind; check it before using the
key, so an RSA public key can never be handed to HMAC as a secret. Check exp and
nbf on every verification, not just at issue time. Compare signatures with
hmac.compare_digest, never ==. Treat kid as an opaque
dict key — normalizing or path-resolving it turns a lookup into a bypass.