JWT libraries keep failing in new ways, and each failure maps to one line of a checklist. In June 2026, Perry (CVE-2026-53776) was fixed for setting validate_exp = false unconditionally. In January, jose-swift (GHSA-88q6-jcjg-hvmw) was fixed for returning true on "alg": "none". On September 28, 2026, PyJWT (CVE-2026-102272) was fixed in 2.14.0 for treating a BOM-prefixed public JWK as an HMAC secret.
What's affected
- Perry before 0.5.1166: expired tokens pass any
jwt.verify()call, so logout and revocation by expiry do not work. - jose-swift up to 6.0.1: fixed in 6.0.2. An attacker can forge any token with an empty signature and
algset tonone. - PyJWT 2.13.0 up to 2.14.0 in a mixed-algorithm verification path: fixed in 2.14.0. An attacker who knows the public key can forge tokens.
What to do now
Upgrade those libraries if you use them, then run your own verify code against this list, which draws on RFC 8725 (the JWT best current practices, February 2020):
- Pin the algorithm list. Pass the exact algorithms you expect. Never read
algfrom the token to decide how to verify it. - Reject
noneunless the caller explicitly asked for it, which almost no API should. - One key, one algorithm. An RSA or EC public key must never be usable as an HMAC secret. If you accept mixed algorithms, separate the key sets.
- Check expiry and not-before, with a small clock skew. Write a test with an expired token and require a rejection.
- Validate
issandaud. RFC 8725 says to requireaudwhen one issuer serves many relying parties, and to reject tokens where it is missing or wrong. - Reject on any cryptographic failure. There is no partial pass.
- Use explicit typing with the
typheader for new token types, so an ID token cannot be replayed as an access token. - Treat revocation as separate. Short expiry plus a server-side deny list for logout and compromised accounts.
A minimal Python check, with the failures you want to see:
import jwt
claims = jwt.decode(
token,
key=PUBLIC_KEY,
algorithms=["RS256"], # pinned, no "none", no HS256 with this key
audience="api.example.test",
issuer="https://id.example.test/",
options={"require": ["exp", "iat", "aud", "iss"]},
)
Then write four tests that must fail: a token with alg set to none, an expired token, a token for another audience, and an HS256 token signed with your public key as the secret.
How this could have been avoided
- A negative test for expiry. Perry's flag disabled the check for every caller, and one expired-token test would have failed.
- A test for
nonein every language you ship. jose-swift's verify returned true before touching a key. - Pinned algorithms on the key. A public key that cannot be used as an HMAC secret closes the PyJWT class of bugs.
- Keep verification in one wrapper in your codebase, so a library upgrade is tested once and the tests are not scattered.
Quick check
Why must the verifier choose the algorithm, not the token header?
If the header decides, an attacker can pick none or switch an asymmetric token to HMAC and sign with a key they know, such as your public key.
Which test would have caught the Perry bug?
Presenting an expired but otherwise valid token and requiring that verification fail.