Step 1 of 5 · Reading · ~3 min
Read
Security & Operations
Defense: Reject alg:none
The most famous JWT vulnerability is not a flaw in any algorithm. It is a feature of the specification, used exactly as written, by a verifier that asked the wrong question first.
Why does none exist at all?
RFC 7519 §8 lists what a conforming implementation is required to support. The
list is remarkably short: HMAC SHA-256 and none. RS256 and ES256 are only
recommended. So every compliant JWT library on earth ships the unsigned mode,
by mandate.
It has a legitimate purpose. RFC 7519 §6 defines an Unsecured JWT for content
already protected by something outside the token — a JWT inside an envelope that is
itself signed, say. It has alg of "none" and the empty string as its
signature, which is why a genuine one ends in a bare dot:
eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UifQ.
Legitimate in that narrow context. Catastrophic anywhere a signature was the only thing standing between a stranger and your database.
The bug, in five lines
Read that early return again. The function has been asked "is this token genuine?" and it answers by consulting the part of the token an attacker fully controls. Forging becomes a text-editing exercise:
Note the trailing dot still yields three segments — the third is the empty
string. A verifier that splits on . and demands exactly three parts is
perfectly happy with this token. Length checks are not a defense.
Defenses, strongest first
- Pin the algorithm in the verifier. The verifier knows what it expects, so
it should say so and never consult the header for the decision:
jwt.decode(token, key, algorithms=["HS256"]). Every modern library — PyJWT, node's jsonwebtoken, jose — now requires this argument for precisely this reason. - Reject the
nonefamily outright, in any casing. - Remove
nonefrom your build if your language lets you.
The case-sensitivity subtlety
RFC 7519 §7.3 says JSON strings are compared exactly and case-sensitively — only
typ and cty are exempt. So strictly, "NoNe" is not the none algorithm;
it is an unrecognised name that a correct verifier rejects as unknown anyway.
Compare case-insensitively anyway. You are not defending against the spec but
against the JSON parser, proxy or downstream library that normalizes case before
your code sees it. Rejecting NoNe costs one .lower().
The hardened verifier
Every rejection happens as early as the information allows, and the signature is the last thing consulted rather than the first.
Your exercise
You will build that verifier: pin HS256, reject everything else.
The ordering above is what the grader tests, and one visible case exists purely
to catch getting it backwards. It hands you a token whose header says
{"alg":"none"} but which carries a non-empty signature segment,
deadbeef, and expects REJECTED alg_none — not bad_signature. If you
compute the HMAC first and only afterwards look at alg, you report the wrong
reason and fail. Decide on the algorithm before you touch the signature.
Three more distinctions the tests pin down: an RS256 header is
REJECTED bad_alg, not alg_none. An HS256 header with an empty third
segment is REJECTED bad_signature. And not.a.token — three segments whose
first is not decodable JSON — is REJECTED bad_token.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…