Skip to content

fix(jwt): exp boundary, and reject NaN and Infinity in time claims - #109

Merged
lepture merged 2 commits into
authlib:mainfrom
carlok:fix/exp-boundary
Nov 3, 2026
Merged

lepture merged 2 commits into
authlib:mainfrom
carlok:fix/exp-boundary

Conversation

@carlok

@carlok carlok commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Two fixes to the time claims in JWTClaimsRegistry, one commit each, so they can be split if you prefer.

1. exp == now. RFC 7519 §4.1.4 defines exp as the time "on or after which the JWT MUST NOT be accepted for processing", as the docstring of validate_exp says. The check raised only when exp < now - leeway, so a token was still accepted during the second in which exp == now (or exp == now - leeway with a leeway). < becomes <=. nbf and iat already follow the RFC. The effect is at most one extra second of validity, the same as a leeway one second larger.

2. NaN and Infinity. NumericDate (RFC 7519 §2) is a JSON number, and NaN and Infinity are not. Python's json reads them anyway (and json.dumps writes them by default), and every comparison with NaN is false, so a token with "exp": NaN was never expired, and neither was "exp": Infinity. nbf and iat accepted NaN the same way. _validate_numeric_time now takes an int, or a float that is finite. An int is always finite, and math.isfinite raises OverflowError for one too large for a float, so ints don't go through it. PyJWT 2.15.1 rejects such an exp with DecodeError.

The token has to be signed, so this needs an issuer that writes a NaN or an Infinity into a claim, not an attacker. For that reason, and for the one-second window in the first fix, I've sent this as a plain PR rather than through the Security tab.

A non-finite value is now an InvalidClaimError ("must be a NumericDate value"). A value that was already refused only changes its error: "exp": -Infinity was ExpiredTokenError.

Tests.

  • test_validate_exp checks both edges, with and without a leeway.
  • test_validate_time_claims_must_be_finite covers exp, nbf and iat with NaN, Infinity and -Infinity.
  • test_decode_then_validate_a_token_that_never_expires goes through jwt.encode and jwt.decode.
  • test_validate_time_claims_accept_large_integers passes with and without the fix; it guards against the OverflowError above.

On main (bc06b26) test_validate_exp, the NaN test and the decode test fail, and they pass with the fixes. pytest (300 passed), ruff check, ruff format --check and mypy are clean.

🤖 Generated with Claude Code

RFC 7519 section 4.1.4: exp is the time on or after which the JWT
MUST NOT be accepted. validate_exp raised only for exp < now - leeway,
so a token was still accepted during the second in which exp == now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NumericDate (RFC 7519 section 2) is a JSON number, and NaN and Infinity
are not. Python's json reads them anyway, and every comparison with NaN
is false, so a token carrying "exp": NaN was never expired, and neither
was "exp": Infinity. nbf and iat accepted NaN the same way.

_validate_numeric_time now accepts an int, or a float that is finite. An
int is always finite, and math.isfinite raises OverflowError for one too
large for a float, so ints do not go through it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@carlok carlok changed the title fix(jwt): reject tokens whose exp equals the current time fix(jwt): exp boundary, and reject NaN and Infinity in time claims Nov 2, 2026
@codecov

codecov Bot commented Nov 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.86%. Comparing base (bc06b26) to head (b8a2394).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #109   +/-   ##
=======================================
  Coverage   99.86%   99.86%           
=======================================
  Files          47       47           
  Lines        3014     3015    +1     
  Branches      362      362           
=======================================
+ Hits         3010     3011    +1     
  Misses          2        2           
  Partials        2        2           
Flag Coverage Δ
unittests 99.86% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lepture
lepture merged commit 6ec88f2 into authlib:main Nov 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants