skip to content

How does the JWT alg:none attack let an attacker forge an accepted token?

level: middleimportance: must knowfreq 68%

answer

  1. The token ends with an empty third part
  2. The header names an algorithm that does nothing
  3. The verifier obeyed instead of checking
  4. Blocking one string is the weak fix
  5. Expected algorithms come from configuration

basics

~20 s

The 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 s

The 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
json
{
  "alg": "none",
  "typ": "JWT"
}

go deeper

for a junior

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.

for a middle

Explain why libraries were vulnerable — dispatching on an attacker-controlled header — and give the structural fix of pinning accepted algorithms in verifier configuration.

for a senior

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.

for a principal

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

context