Safeguard
Application Security•

A JWT Validation Checklist, From Three 2026 Library Bugs

Perry skipped expiry checks, jose-swift accepted alg none, PyJWT trusted a public key as a secret. One checklist that would have caught all three in your own verify code.

Safeguard Security Team
3 min read

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 alg set to none.
  • 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):

  1. Pin the algorithm list. Pass the exact algorithms you expect. Never read alg from the token to decide how to verify it.
  2. Reject none unless the caller explicitly asked for it, which almost no API should.
  3. 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.
  4. Check expiry and not-before, with a small clock skew. Write a test with an expired token and require a rejection.
  5. Validate iss and aud. RFC 8725 says to require aud when one issuer serves many relying parties, and to reject tokens where it is missing or wrong.
  6. Reject on any cryptographic failure. There is no partial pass.
  7. Use explicit typing with the typ header for new token types, so an ID token cannot be replayed as an access token.
  8. 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 none in 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.