Skip to content

fix: reject NumericDates outside Instant range - #790

Open
ANSHUL-REAL wants to merge 2 commits into
auth0:masterfrom
ANSHUL-REAL:fix/out-of-range-numeric-date
Open

fix: reject NumericDates outside Instant range#790
ANSHUL-REAL wants to merge 2 commits into
auth0:masterfrom
ANSHUL-REAL:fix/out-of-range-numeric-date

Conversation

@ANSHUL-REAL

@ANSHUL-REAL ANSHUL-REAL commented Jul 24, 2026

Copy link
Copy Markdown

Fixes #782

Registered NumericDate claims that fit in a long but fall outside java.time.Instant's epoch-second range are now rejected through the library's existing JWTDecodeException boundary.

The range check uses Instant.MIN and Instant.MAX directly and covers both overflow and underflow with regression tests for Long.MAX_VALUE and Long.MIN_VALUE.

Validation:

  • ./gradlew :java-jwt:test --tests com.auth0.jwt.impl.PayloadDeserializerTest -x :java-jwt:compileModuleInfoJava
  • ./gradlew :java-jwt:test -x :java-jwt:compileModuleInfoJava

@ANSHUL-REAL
ANSHUL-REAL requested a review from a team as a code owner July 24, 2026 09:24
@Zichen1028

Zichen1028 commented Aug 2, 2026

Copy link
Copy Markdown

Hi @ANSHUL-REAL,

Thanks for tracking this down — the fix correctly closes the gap from #782, and the repro in the issue was really helpful.

One small gap in test coverage: Instant's range check is symmetric (seconds < MIN_SECOND || seconds > MAX_SECOND), but the new test only covers the upper bound (Long.MAX_VALUE). It'd be worth adding a mirrored case for the lower bound (e.g. Long.MIN_VALUE) to confirm the catch handles underflow the same way it handles overflow — right now that's assumed rather than verified.

Separately, and non-blocking for this PR: getInstantFromSeconds has now grown three independent guards for essentially the same question ("is this value a valid NumericDate") — !node.isNumber() and !node.canConvertToLong() from #706, and now this catch (DateTimeException) for #782. Each was added reactively when a new edge case surfaced, which makes me think there may be a fourth case waiting somewhere (e.g. if canConvertToLong()'s semantics ever shift, or another scientific-notation edge case turns up).

Would it be worth consolidating the range check into a single explicit check against Instant.MIN/Instant.MAX, rather than relying on Instant.ofEpochSecond to throw as a side effect? Something like:

if (!node.canConvertToLong()
        || node.asLong() < Instant.MIN.getEpochSecond()
        || node.asLong() > Instant.MAX.getEpochSecond()) {
    throw new JWTDecodeException(String.format(
            "The claim '%s' value (%s) is out of the range representable as a NumericDate.",
            claimName, node.asText()));
}
return Instant.ofEpochSecond(node.asLong());

This keeps the exact same exception types and messages (so no behaviour change for callers), removes the try/catch, and ties the bound directly to Instant's own public constants instead of relying on catching its internal validation. Happy to open this as a separate follow-up issue/PR if you'd rather keep this one focused on #782 — just flagging it here since I ran into it while reviewing.

@ANSHUL-REAL
ANSHUL-REAL force-pushed the fix/out-of-range-numeric-date branch from f5078ea to 8ec08a9 Compare August 2, 2026 12:42
@Zichen1028

Zichen1028 commented Aug 2, 2026

Copy link
Copy Markdown

Verified locally in both directions (Long.MAX_VALUE and Long.MIN_VALUE); both are now covered and passing. The Instant.MIN/Instant.MAX approach reads a lot cleaner than the try/catch too. Thanks for making these changes! @ANSHUL-REAL

Hi @ANSHUL-REAL,

Thanks for tracking this down — the fix correctly closes the gap from #782, and the repro in the issue was really helpful.

One small gap in test coverage: Instant's range check is symmetric (seconds < MIN_SECOND || seconds > MAX_SECOND), but the new test only covers the upper bound (Long.MAX_VALUE). It'd be worth adding a mirrored case for the lower bound (e.g. Long.MIN_VALUE) to confirm the catch handles underflow the same way it handles overflow — right now that's assumed rather than verified.

Separately, and non-blocking for this PR: getInstantFromSeconds has now grown three independent guards for essentially the same question ("is this value a valid NumericDate") — !node.isNumber() and !node.canConvertToLong() from #706, and now this catch (DateTimeException) for #782. Each was added reactively when a new edge case surfaced, which makes me think there may be a fourth case waiting somewhere (e.g. if canConvertToLong()'s semantics ever shift, or another scientific-notation edge case turns up).

Would it be worth consolidating the range check into a single explicit check against Instant.MIN/Instant.MAX, rather than relying on Instant.ofEpochSecond to throw as a side effect? Something like:

if (!node.canConvertToLong()
        || node.asLong() < Instant.MIN.getEpochSecond()
        || node.asLong() > Instant.MAX.getEpochSecond()) {
    throw new JWTDecodeException(String.format(
            "The claim '%s' value (%s) is out of the range representable as a NumericDate.",
            claimName, node.asText()));
}
return Instant.ofEpochSecond(node.asLong());

This keeps the exact same exception types and messages (so no behaviour change for callers), removes the try/catch, and ties the bound directly to Instant's own public constants instead of relying on catching its internal validation. Happy to open this as a separate follow-up issue/PR if you'd rather keep this one focused on #782 — just flagging it here since I ran into it while reviewing.

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.

Out-of-range NumericDate claims can throw unchecked DateTimeException during verification

2 participants