The alg:none attack forges a JWT by setting the token’s header algorithm to “none”, which tells a carelessly written verifier that the token is unsigned and should be trusted without checking any signature. When it works, an attacker can hand your server a token claiming to be any user, with any privileges, and the server accepts it, because it asked the attacker’s own token how it ought to be verified and the token answered “you don’t need to.” The bug is a textbook case of trusting attacker-controlled input to make a security decision, and the fix is correspondingly simple once you see the mistake.
What does a JWT signature normally protect?
A JWT’s signature is the only thing that makes its contents trustworthy, because the rest of the token is merely encoded, not protected. A JSON Web Token has three parts separated by dots: a header, a payload, and a signature, each base64url-encoded. The header names the algorithm; the payload holds the claims such as the user ID and expiry; the signature is computed over the header and payload with a key. Because the first two parts are only encoded, anyone can read and rewrite them. What stops a rewritten token from being accepted is that the signature will no longer verify.
header.payload.signature
header: {"alg":"HS256","typ":"JWT"} base64url-encoded
payload: {"sub":"1234","role":"user"} base64url-encoded
signature: HMAC-SHA256(header.payload, secret) the trust anchor
So verification is the heart of the system. When a token arrives, the server recomputes the signature using its key and the token’s header and payload, and compares. If they match, the header and payload are authentic; if not, the token is rejected. Every claim the server later relies on, who the user is, what they may do, is only as trustworthy as that signature check. Remove or bypass the check and the claims become whatever the sender wants. You can look at any token’s three parts with our JWT decoder, which decodes them in your browser and makes plain that the payload is readable and, without verification, malleable.
How does the alg:none attack work?
The attack works by exploiting a verifier that reads the algorithm from the token’s own header and obeys it, even when the header says “none”. The JWS specification does define a “none” algorithm for unsecured tokens, meaning a token with no signature at all, intended for cases where integrity is assured by some other mechanism. A none-algorithm token has an empty third segment: there is simply nothing after the second dot, because there is no signature.
The vulnerability appears when a server’s verification code effectively asks the token how to verify itself. A naive implementation decodes the header, sees "alg":"none", concludes that this token is unsigned by design, and therefore skips the signature check entirely, treating the payload as valid. An attacker who understands this simply takes a legitimate token structure, rewrites the payload to claim whatever they want, sets the header algorithm to “none”, drops the signature, and sends it. The server, following the header’s instruction, performs no check and accepts the forged claims.
Attacker-crafted token (note the empty signature):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
.
header -> {"alg":"none","typ":"JWT"}
payload -> {"sub":"1234","role":"admin"} <- attacker set role to admin
signature -> (empty; nothing after the last dot)
A verifier that honors alg:none skips the check and trusts role=admin.
The elegance, from the attacker’s side, is that no key is needed. They are not breaking the cryptography; they are convincing the server not to use any. The root cause is that a value under the attacker’s control, the header alg field, was allowed to dictate the security decision. That is the same category of error as trusting a user-supplied flag to decide whether to run an authorization check.
It is worth stressing how little sophistication the attack requires. There is no brute forcing, no leaked secret, and no cryptographic weakness to exploit. Any tool that can base64url-encode a small piece of JSON is enough to build the forged token, and the attacker only needs to know the rough shape of the claims your application expects. If your tokens carry a role or an isAdmin field, they simply set it to whatever grants access. The barrier to entry is essentially zero, which is exactly why a verifier that honors “none” is so dangerous: the pool of people who can exploit it is everyone.
What is the related algorithm-confusion attack?
Algorithm confusion is a closely related bug in which the attacker switches the header to a different real algorithm to trick the verifier into using the wrong key, most famously turning RS256 into HS256. RS256 is asymmetric: tokens are signed with a private key and verified with a public key, and the public key is, by definition, public. HS256 is symmetric: the same secret both signs and verifies. The attack abuses a verifier that picks its verification method from the header without pinning the expected one.
Here is the trick. The server expects RS256 and verifies with its public key. The attacker changes the header to say HS256, then signs a forged token using the server’s public key as if it were the HMAC secret. When the flawed verifier reads “HS256” from the header, it runs HMAC verification, and the only key it has on hand for that token is the RSA public key, which it now feeds into HMAC as the secret. Because the attacker used that very same public value to sign, the HMAC check passes. The publicly known key has been turned into a valid signing secret.
Both alg:none and algorithm confusion share one root cause: the verifier let the token’s header decide which algorithm, and thus which trust model, to apply. Once you see that they are the same mistake in two costumes, the defense against both becomes obvious.
How do you shut it down?
You shut down both attacks by never letting the token header choose the verification algorithm, and instead pinning the algorithm the server will accept. The verifier should be configured with an explicit allowlist, “this endpoint accepts HS256 and nothing else,” and must reject any token whose header disagrees, including “none”. The algorithm is a decision the server makes based on how it issued the token, not a hint it takes from the incoming token. The table lays out the contrast.
| Behavior | Vulnerable verifier | Hardened verifier |
|---|---|---|
| Source of algorithm | Read from token header | Fixed by server configuration |
| Token claims alg:none | Skips signature check | Rejected outright |
| Header alg differs from expected | Follows the header | Rejected as invalid |
| RS256 to HS256 swap | Verifies with wrong key type | Rejected; only RS256 accepted |
| Trust decision owner | The attacker (via header) | The server |
Concretely, when you call your JWT library’s verify function, pass the expected algorithm explicitly rather than relying on it to infer one. Reputable libraries have hardened against silently accepting “none” and against naive key reuse, but a misconfiguration or an old default can still reopen the door, so make the expected algorithm an explicit, reviewed part of your code. Keep signing keys secret and well-managed, and never confuse a public verification key with a signing secret.
As an operational habit, decode tokens during development and incident response to see exactly what an endpoint is being handed. Reading the header’s alg field tells you immediately whether a token claims to be unsigned, and an empty third segment is the visible fingerprint of a none-algorithm token. Doing that inspection with a client-side decoder means the token, which is a credential, is never transmitted while you examine it. The underlying discipline is one line long: the server, not the token, decides how the token is verified. Hold to that and alg:none has nothing to exploit.