What is the alg none attack on JWT?

Dark navy cover card with the title 'What is the alg none attack on JWT?' in white and a green accent bar

The JWT alg none attack is a forged token whose header says "alg": "none" and whose signature is empty. If the server's JWT library trusts the header, it skips the signature check and accepts whatever claims the attacker wrote, such as "role": "admin". The fix is to tell your verify call which algorithms you expect and reject everything else.

How the JWT alg none attack works

The alg none attack works because a signed JWT tells the receiver which algorithm was used, in its own header. A library that reads that header and obeys it lets the attacker pick "no signature at all."

RFC 8725, the JWT best practices document, describes the problem in one line: the algorithm "can be changed to "none" by an attacker, and some libraries would trust this value and "validate" the JWT without checking any signature."

The attack takes four steps:

  1. Grab any valid token, for example your own session token.
  2. Decode the header and payload, change the header to {"alg":"none"} and edit the payload claims.
  3. Base64url-encode both parts again.
  4. Drop the signature but keep the final dot, then send the token.

Keep that dot. PortSwigger's JWT guide notes that "the payload part must still be terminated with a trailing dot." A token with only one dot fails parsing before the algorithm is ever read.

What a real unsecured JWT looks like

An unsecured JWT is a normal JWT with alg set to none and an empty third part. The JWT spec, RFC 7519, defines it this way: "a JWS using the "alg" Header Parameter value "none" and with the empty string for its JWS Signature value."

The spec's own example in Section 6.1 has this header, which encodes to eyJhbGciOiJub25lIn0:

{"alg":"none"}

So a forged token has this shape. Nothing follows the last dot:

eyJhbGciOiJub25lIn0.<base64url payload>.

You can paste a token like this into a decoder to see it. Our JWT decoder shows the header, payload and signature parts side by side. It decodes and never verifies, so it is the wrong place to make a trust decision. See JWT decode vs verify.

Why the spec requires "none" but forbids trusting it

The JWT spec requires every conforming library to implement none, and a sibling spec forbids accepting it by default. Both rules are real, and the bug lives in the gap between them.

RFC 7519 Section 8 says "only HMAC SHA-256 ("HS256") and "none" MUST be implemented by conforming JWT implementations." The algorithms spec, RFC 7518 Section 3.6, then says "Implementations MUST NOT accept Unsecured JWSs by default." It also says apps must not allow unsigned tokens at a global level, only per object.

Read together, a library has to know none but must refuse it unless the caller asks for it. RFC 8725 repeats this: libraries "SHOULD NOT consume JWTs using "none" unless explicitly requested by the caller."

The early libraries got the first rule and missed the second. Tim McLean's 2015 disclosure on the Auth0 blog found that "some libraries treated tokens signed with the none algorithm as a valid token with a verified signature." He named node-jsonwebtoken, pyjwt, namshi/jose, php-jwt and jsjwt in the affected list.

Why a simple "reject none" check isn't enough

A check that blocks the literal string none stops the textbook attack but not the root cause. The root cause is that the token, not your server, picks the algorithm.

Case and encoding tricks. PortSwigger points out that "none" filters rely on string parsing, so they can sometimes be bypassed with "mixed capitalization and unexpected encodings." A blocklist that checks alg === "none" misses "None" or "nOnE" if a lower layer compares without case.

Algorithm confusion. The same design flaw enables a second attack. RFC 8725 describes switching RS256 to HS256, so a library verifies an HMAC using the RSA public key as the shared secret. As McLean wrote, blocking none "doesn't solve the underlying problem: attackers control the choice of algorithm." Our HS256 vs RS256 guide explains why mixing the two key types is dangerous.

How to prevent the alg none attack

You prevent the alg none attack by deciding the algorithm on the server and passing it to the verify function as an allowlist. RFC 8725 Section 3.1 makes this a library requirement: libraries "MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms."

In Node with jsonwebtoken, that means always passing algorithms:

// Only RS256 is accepted; "none" and HS256 are rejected.
const claims = jwt.verify(token, publicKey, { algorithms: ['RS256'] });

A Semgrep rule for jsonwebtoken flags any verify call whose algorithms list includes 'none'. Running a rule like this in CI catches the mistake before review.

How common setups hold up:

Setup Safe against alg none? Why
verify with an explicit algorithms list, no none Yes The server picks the algorithm
verify with a key but no algorithms list Depends on the library The header may still choose between key types
Blocklist that rejects the string "none" Partly Misses case tricks and algorithm confusion
decode and read claims, no verify No Nothing is checked at all
Hand-written split, base64 decode, compare Usually no Easy to skip the empty-signature case

My take: treat the alg header as a hint you check, never as an instruction you follow. If your server signs every token with one algorithm, allow exactly that one. McLean's own suggestion was a verify call shaped like verify(token, algorithm, verificationKey), because "the server should already know what algorithm it uses."

The limitation: pinning the algorithm stops none and confusion attacks and does nothing for a weak HMAC secret or a missing exp check. You still need the rest of verification. Our guide on how to decode a JWT safely covers the claim checks that come after the signature.

How to test your own API for it

You can test for the alg none attack in a few minutes against a staging server you own. Take a valid token, rewrite it as shown above, and call an endpoint that needs authentication.

  • An "unauthorized" or "invalid algorithm" error means the library rejected it.
  • A normal response means your server accepted an unsigned token. Treat that as a critical bug.
  • Repeat with case variants such as "None", since PortSwigger notes filters can miss mixed capitalization.

Only run this against systems you own or are authorised to test. If you just need to read a token's header while debugging, our post on whether a JWT can be decoded without the secret explains why no key is needed for that.

FAQ

Is alg none ever legitimate?

Yes, in narrow cases. RFC 8725 says none should only be used when the JWT is protected by other means, such as a signature on a structure that contains the token. A token arriving from a browser or API client is never that case.

Do modern JWT libraries still accept alg none?

Most reject it when you pass a key, and the JWA spec says implementations must not accept unsigned tokens by default. Hand-written validators and loose configurations can still accept it, so pin the allowed algorithms yourself.

What does an alg none token look like?

The header decodes to {"alg":"none"}, the payload is normal base64url JSON, and the signature part is empty, so the token ends with a dot. RFC 7519 Section 6.1 shows a full example.

Is the alg none attack the same as algorithm confusion?

No, but both come from the same flaw: letting the token choose its own algorithm. Alg none removes the signature. Algorithm confusion switches RS256 to HS256 so the public key is used as an HMAC secret.

Sources