The jsonwebtoken npm package is the most widely used library for creating and verifying JSON Web Tokens in Node.js, and versions at or below 8.5.1 contain several flaws in jwt.verify() that can undermine token security, all fixed in version 9.0.0. Because npm jsonwebtoken sits directly on your authentication path, an issue here is not a nuisance dependency bug, it is a potential authentication bypass, which is why the December 2022 advisory batch got so much attention.
Getting this one right is worth the few minutes it takes.
What jsonwebtoken does
A JSON Web Token is a signed, self-contained credential: a header, a payload of claims, and a signature. jsonwebtoken handles the two operations that matter, jwt.sign() to issue a token and jwt.verify() to validate one. The security of the whole scheme rests on verify being strict about the signature and the algorithm, because a token whose signature is not properly checked is just an attacker-editable cookie.
That is exactly where the historical vulnerabilities lived.
The pre-9.0.0 vulnerabilities
In December 2022, Auth0 (the maintainer) released jsonwebtoken 9.0.0 to address four CVEs, all affecting versions at or below 8.5.1:
- CVE-2022-23529: insecure input validation in
jwt.verify()that, under specific conditions, could be exploited when a secret or public key was passed in an unexpected form. - CVE-2022-23539: the library allowed insecure legacy key types to be used for signature verification.
- CVE-2022-23540: an insecure default algorithm in
jwt.verify()that could lead to a signature-validation bypass. Version 9.0.0 removed the implicit acceptance of thenonealgorithm. - CVE-2022-23541: an insecure key-retrieval implementation that could permit forged tokens by confusing RSA and HMAC key handling.
The common thread is the "algorithm confusion" family of attacks. If a verifier does not pin the expected algorithm, an attacker can sometimes present a token signed with a different algorithm, or the none algorithm, and slip past verification. Version 9.0.0 tightened these defaults, and the remediation is simply to upgrade.
The fix and the safe usage pattern
First, upgrade:
npm install jsonwebtoken@^9.0.0
Then, regardless of version, verify tokens defensively. The most important rule is to always specify the exact algorithm you expect. Never let the library infer it from the token itself, because that is what enables algorithm-confusion attacks:
const jwt = require('jsonwebtoken');
// Good: pin the algorithm and set an expiry check
try {
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // explicit allowlist, not inferred
issuer: 'https://auth.example.com',
audience: 'my-api',
maxAge: '15m'
});
// payload is trustworthy here
} catch (err) {
// reject: signature, expiry, issuer, or audience failed
}
The algorithms option is the single most important line. Passing an explicit allowlist means a token claiming a different algorithm is rejected outright. Adding issuer and audience checks ensures a valid token minted for another service cannot be replayed against yours.
For signing, keep secrets strong and out of source control:
const token = jwt.sign(
{ sub: user.id, role: user.role },
privateKey,
{ algorithm: 'RS256', expiresIn: '15m' }
);
Prefer asymmetric algorithms (RS256, ES256) when the verifier and signer are different services, so the verifier only needs the public key. Reserve symmetric HS256 for cases where one party both signs and verifies.
Common mistakes beyond the CVEs
Upgrading fixes the library flaws, but most JWT incidents come from how the library is used:
- Decoding instead of verifying.
jwt.decode()reads the payload without checking the signature. It is for inspection only. Using it to make authorization decisions means trusting attacker-supplied claims. Always usejwt.verify()for anything security-relevant. - No expiry. Tokens without
expiresInlive forever. Keep access tokens short and use refresh tokens for longevity. - Secrets in the repo. An HS256 secret committed to Git lets anyone forge valid tokens. Load keys from a secret manager or environment configuration.
- Trusting
algfrom the token. Covered above, but worth repeating: pin algorithms inverify.
Keeping the dependency current
jsonwebtoken is often transitive, pulled in by auth middleware and SDKs, so a vulnerable copy can exist even if you install version 9.x directly. Check the whole tree:
npm ls jsonwebtoken
npm why jsonwebtoken
Continuous scanning is what keeps this honest over time. An SCA tool such as Safeguard flags a jsonwebtoken at or below 8.5.1 anywhere in your dependency graph and can gate a build on it, so an old copy dragged in by a transitive parent does not quietly reopen an authentication weakness. Given that this library sits on the auth path, it is worth setting a strict severity threshold for it specifically. Our academy has a walkthrough on JWT hardening that goes deeper on key rotation and revocation.
FAQ
Which jsonwebtoken version fixes the known vulnerabilities?
Version 9.0.0, released in December 2022, fixed CVE-2022-23529, CVE-2022-23539, CVE-2022-23540, and CVE-2022-23541. All affected versions at or below 8.5.1. Upgrade to 9.0.0 or later.
What is the most important option when verifying a JWT?
The algorithms allowlist. Always pass the exact algorithm you expect to jwt.verify() so a token claiming a different algorithm, or the none algorithm, is rejected. This closes the algorithm-confusion class of attacks.
Is jwt.decode() safe to use for authorization?
No. jwt.decode() reads the payload without verifying the signature, so its claims are attacker-controlled. Use it only for inspection. Always use jwt.verify() for any security decision.
How do I find a vulnerable jsonwebtoken pulled in transitively?
Run npm ls jsonwebtoken to list all copies and npm why jsonwebtoken to see which parent introduced an old one. Software composition analysis will flag copies at or below 8.5.1 anywhere in the resolved tree, including transitive ones.