Skip to content
Lesson 10 of 14

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

python

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:

python

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

  1. 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.
  2. Reject the none family outright, in any casing.
  3. Remove none from 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

Rendering diagram…

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.

Up nextJWKS: Key Set + kid LookupSecurity & Operations

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…