JWT "alg": "none": forging an admin token
A verifier that trusts the algorithm named in the token header can be told to skip the signature check.
A JWT is base64url(header).base64url(payload).signature. The header and payload are encoded, not encrypted: anyone can read and edit them. The signature, here HMAC-SHA256 with a server-side secret, is the only integrity protection.
{
"alg": ,
"typ": "JWT"
}{
"sub": "alice",
"role": ,
"exp": 1893456000
}Click the underlined values. Make yourself admin: the signature no longer matches. Then tell the server the token needs no signature.
{
"error": "admin only"
}claims, err := jwt.Verify(token, key) // uses the token's own "alg": "none" skips the checkThe bug
Changing "role": "user" to "admin" invalidates the HS256 signature, and the server rejects the token. But the JWT spec (RFC 7519) also defines unsecured tokens: "alg": "none" with an empty signature. A verifier that picks the algorithm from the token header lets the attacker choose none, and the forged payload is accepted.
claims, err := jwt.Verify(token, key) // algorithm read from the token header Several JWT libraries behaved like this until it was disclosed in 2015. The pattern still shows up in hand-written verification code and in old dependencies.
Related issues
- Algorithm confusion. An RS256 verifier that also accepts HS256 will validate an HMAC computed with the RSA public key as the secret.
- Weak HMAC secrets. A single token is enough for an offline brute force (hashcat mode 16500).
decodeinstead ofverify. The payload is read but the signature is never checked.- No
expcheck and no revocation. A leaked token stays valid indefinitely. - Sensitive data in the payload. It is readable by whoever holds the token.
Fix
Pin the algorithm on the server and require an expiry. With golang-jwt v5:
token, err := jwt.Parse(raw, func(t *jwt.Token) (any, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"}), jwt.WithExpirationRequired())
if err != nil || !token.Valid {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
} For sensitive operations, read the role from the database rather than from the token. Claims reflect the state when the token was issued.
I test web apps, APIs and LLM integrations. Contact.
Next article → MITM: an API that trusts the client’s price