How does the JWT alg:none attack let an attacker forge an accepted token?
answer
- The token ends with an empty third part
- The header names an algorithm that does nothing
- The verifier obeyed instead of checking
- Blocking one string is the weak fix
- Expected algorithms come from configuration
basics
~20 sThe attacker edits the payload, sets the JOSE header's alg to none, and sends the token with an empty signature. A verifier that obeys the header's algorithm instead of its own configuration runs no signature check and accepts the forged claims.
solid answer
~50 sThe JOSE specification defines `none` as the algorithm for an *unsecured* token — one with an empty signature segment, intended for cases where integrity is guaranteed by other means. The attack exploits verifiers that read `alg` from the incoming header and dispatch on it: the attacker takes any real token, rewrites the claims to whatever they want, sets `"alg": "none"`, drops the signature so the token ends in a trailing dot, and sends it. The verifier looks up the `none` handler, which verifies nothing, and every forged claim is accepted. Several early libraries shipped exactly this behaviour. The fix is structural, not a filter: the verifier decides the acceptable algorithms from its own configuration and rejects anything else, so `none` is never reachable at runtime — and libraries should require an explicit opt-in before an unsecured token is ever honoured.
code
json · 4 lines{
"alg": "none",
"typ": "JWT"
}go deeper
Recognise the shape: a token whose header says the algorithm is none and whose signature segment is empty. Know that a verifier must never take the algorithm from the token.
Explain why libraries were vulnerable — dispatching on an attacker-controlled header — and give the structural fix of pinning accepted algorithms in verifier configuration.
Discuss where the pattern reappears today: hand-rolled gateway or serverless authorizers, decode-only components whose output is trusted downstream, and the negative test that keeps it closed after upgrades.
Own the general rule across the platform — no component may let a credential influence how it is verified — and make expected-algorithm configuration a reviewed default rather than a per-service decision.
## What `none` legitimately means JSON Web Signature defines an algorithm value `none`, which produces an **unsecured JWS**: the signature segment is empty and no integrity protection is applied. It exists for situations where the token's integrity is guaranteed by the surrounding transport or by a nested structure — for example a token already wrapped in an encrypted envelope. It is a specified, legitimate value; the danger is not that it exists but that a verifier can be steered into using it. ## The attack, step by step 1. The attacker obtains any valid token for the target system — often their own low-privilege token. 2. They Base64url-decode the payload and change it: a different `sub`, an elevated role, a longer `exp`, a different tenant. 3. They rewrite the header to `{"alg":"none","typ":"JWT"}` and Base64url-encode it. 4. They assemble `header.payload.` — three segments, the third empty, so the string ends in a dot. 5. They present it. A vulnerable verifier parses the header, reads `alg`, selects the handler for that name, and calls it. The `none` handler's contract is that there is nothing to check, so it returns success. Claim validation then runs on attacker-authored claims that the verifier now believes are authentic. The attacker has authenticated as anyone. ## Why real libraries had this bug The root cause is a plausible-looking API design. A function shaped like `verify(token, key)` has to decide which algorithm to run, and the token conveniently carries that information — so early implementations dispatched on the header value. That works perfectly against honest tokens and fails completely against hostile ones, because the header is attacker-controlled input, not issuer configuration. A cluster of libraries across several ecosystems shipped this behaviour around 2015, and the disclosure is why modern libraries require the caller to state the expected algorithm. A second variant is case and type sloppiness: a verifier that blocks the literal string `none` but not `None`, `NONE`, or a JSON value that is not a string at all. Filtering the bad value is the weak fix; only accepting known-good values is the strong one. ## The correct defence **Pin the algorithm at the verifier.** Configuration says "tokens from this issuer are RS256"; verification compares the token's `alg` against that expectation and rejects on mismatch, before any key lookup. The token's header becomes an assertion you check rather than an instruction you follow, and `none` is unreachable regardless of what arrives. Supporting practices: - **Use a library API that demands the expected algorithm** as an explicit parameter, and treat any API that will infer it as a hazard. - **Never register a `none` handler** in a production verification path; if the library supports unsecured tokens, ensure that requires a distinct, explicit opt-in. - **Reject structurally odd tokens early** — an empty signature segment has no legitimate reason to appear on an authentication path, so it can be rejected before algorithm handling even runs. - **Test for it.** A negative test that constructs an `alg: none` token with an inflated role claim and asserts a rejection is cheap, and it will catch a library upgrade or a configuration change that silently reopens the hole. ## Why "nobody would ship that today" is the wrong conclusion Mainstream libraries fixed this years ago, but the pattern recurs whenever someone writes verification by hand — in a gateway plugin, a serverless authorizer, a test double promoted into production, or a hand-rolled parser inside a service mesh filter. It also recurs at the wrong layer: a component that only *decodes* tokens for logging or routing, and whose output is later trusted by something downstream, is functionally equivalent to a verifier with no signature check. ## The general principle The security lesson generalises past this one value: **a token must never choose how it is verified**. Every parameter that influences verification — algorithm, key, key source — is the verifier's to decide. The header may carry hints (`kid` is a lookup key, `alg` is a claim about what was done), but a hint is something you match against your own expectations, never something you obey. Every JWT attack in this family, including algorithm confusion and key-source injection, is a different instance of that one mistake. ## Answering it well Give the mechanism concretely (empty third segment, header rewritten), name the root cause (dispatching on attacker-controlled input), and give the structural fix (verifier-pinned algorithms) rather than a blocklist. Mentioning that filtering the string `none` case-insensitively is the weak fix shows you understand allowlisting versus denylisting.
- Is blocking the literal string "none" a sufficient fix?No. It is a denylist, so it misses `None`, `NONE`, non-string JSON values, and any future variant, and it leaves the underlying design intact — the token still chooses the algorithm. The correct fix is an allowlist: the verifier states which algorithms it accepts for an issuer and rejects everything else, which closes this and related substitution attacks at once.
- If alg:none is specified behaviour, why is it in the specification at all?It describes an unsecured JWS, for contexts where integrity is already guaranteed by something else — for example a token nested inside an encrypted envelope, or passed over a channel that authenticates it. The specification is explicit that such tokens carry no integrity protection of their own, so accepting one on an authentication path is a misuse of the feature, not a flaw in it.
- How would you write a regression test that proves your service is not vulnerable?Construct a token by hand: Base64url-encode a header with `alg` set to `none`, Base64url-encode a payload with an elevated claim, join them with a dot and append a trailing dot for the empty signature. Assert the endpoint returns an authentication failure. Repeat with mixed-case variants so a denylist fix cannot pass the test.
saying these in an interview costs you the question
- Selects the verification algorithm from the token's header
- Says an empty signature is harmless because the payload is signed
- Fixes it by string-matching none case-sensitively
- Assumes the library is safe without passing an expected algorithm
- Treats a decode-only component as if it verified the token