+ "details": "_NOTE_: While the library exposes a mechanism which could introduce the vulnerability, this issue is created by developer-supplied code and not by the library itself. We will add a warning and some education for users around the possible issues however since the defaults work we will not be updating the library beyond that for this advisory.\n\n## Impact\n\nSetting up a custom cacheKeyBuilder method which does not properly create unique keys for different tokens can lead to cache collisions. This could cause tokens to be mis-identified during the verification process leading to:\n\n- Valid tokens returning claims from different valid tokens\n- Users being mis-identified as other users based on the wrong token\n\nThis could result in:\n- User impersonation - UserB receives UserA's identity and permissions\n- Privilege escalation - Low-privilege users inherit admin-level access\n- Cross-tenant data access - Users gain access to other tenants' resources\n- Authorization bypass - Security decisions made on wrong user identity\n\n## Affected Configurations\n\nThis vulnerability ONLY affects applications that BOTH:\n\n1. Enable caching using the cache option\n2. Use custom cacheKeyBuilder functions that can produce collisions\n\nVULNERABLE examples:\n```\n// Collision-prone: same audience = same cache key\ncacheKeyBuilder: (token) => {\n const { aud } = parseToken(token)\n return `aud=${aud}`\n}\n\n// Collision-prone: grouping by user type\ncacheKeyBuilder: (token) => {\n const { aud } = parseToken(token)\n return aud.includes('admin') ? 'admin-users' : 'regular-users'\n}\n\n// Collision-prone: tenant + service grouping\ncacheKeyBuilder: (token) => {\n const { iss, aud } = parseToken(token)\n return `${iss}-${aud}`\n}\n```\n\nSAFE examples:\n```\n// Default hash-based (recommended)\ncreateVerifier({ cache: true }) // Uses secure default\n\n// Include unique user identifier\ncacheKeyBuilder: (token) => {\n const { sub, aud, iat } = parseToken(token)\n return `${sub}-${aud}-${iat}`\n}\n\n// No caching (always safe)\ncreateVerifier({ cache: false })\n```\n### Not Affected\n\n- Applications using **default caching**\n- Applications with **caching disabled**\n \n## Assessment Guide\n\nTo determine if you're affected:\n\n1. Check if caching is enabled: Look for cache: true or cache: <number> in verifier configuration\n2. Check for custom cache key builders: Look for cacheKeyBuilder function in configuration\n3. Analyze collision potential: Review if your cacheKeyBuilder can produce identical keys for different users/tokens\n4. If no custom cacheKeyBuilder: You are NOT affected (default is safe)\n\n## Mitigations\n\nMitigations include:\n\n- Ensure uniqueness of keys produced in cacheKeyBuilder\n- Remove custom cacheKeyBuilder method\n- Disable caching\n\n---\n\nfast-jwt allows enabling a verification cache through the cache option.\nThe cache key is derived from the token via cacheKeyBuilder.\n\nWhen a custom cacheKeyBuilder produces collisions between different tokens, the verifier may return the cached payload of a previous token instead of validating and returning the payload of the current token.\n\nThis results in cross-token payload reuse and identity confusion.\n\nTwo distinct valid JWTs can be verified successfully but mapped to the same cached entry, causing the verifier to return claims belonging to a different token.\n\nThis affects authentication and authorization decisions when applications trust the returned payload.\n\nAffected component\n\nsrc/verifier.js\n\nRelevant logic:\n\ncache enabled via createCache\n\ncache population via cacheSet\n\nlookup based on cacheKeyBuilder(token)\n\ncached payload returned without re-verification\n\nImpact\n\nIdentity / authorization confusion via cache collision.\n\nIf two tokens generate the same cache key:\n\ntoken A is verified → payload stored in cache\n\ntoken B is verified → cache hit occurs\n\nverifier returns payload from token A instead of B\n\nObserved effect:\n\nsubject mismatch\n\nclaim mismatch\n\nauthorization decision performed on wrong identity\n\nPotential real-world consequences:\n\nuser impersonation (logical)\n\nprivilege confusion\n\nincorrect RBAC evaluation\n\ngateway / middleware auth inconsistencies\n\nThis is especially dangerous when:\n\ncache is enabled (recommended for performance)\n\ncustom cacheKeyBuilder is used\n\nidentity claims (sub / aud / iss) drive authorization\n\nRoot cause\n\nThe verifier assumes the cache key uniquely identifies the token and its claims.\n\nHowever:\n\ncacheKeyBuilder is user-controlled\n\ncollisions are not detected\n\ncache entries store decoded payload\n\ncached payload is returned without binding validation\n\nThis creates a trust boundary break between:\n\ntoken → cache key → cached payload\n\nProof of concept\n\nEnvironment:\n\nfast-jwt: 6.1.0\n\nNode.js: v24.13.1\n\nPoC:\n\nconst { createSigner, createVerifier } = require('fast-jwt')\n\nconst sign = createSigner({ key: 'secret' })\n\n// Two distinct tokens\nconst t1 = sign({ sub: 'userA', aud: 'admin' })\nconst t2 = sign({ sub: 'userB', aud: 'admin' })\n\n// Deliberately unsafe cache key builder (collision)\nconst verify = createVerifier({\n key: 'secret',\n cache: true,\n cacheKeyBuilder: () => 'static-key'\n})\n\nconsole.log('verify t1')\nconst p1 = verify(t1)\nconsole.log('t1 PASS sub=', p1.sub)\n\nconsole.log('verify t2')\nconst p2 = verify(t2)\nconsole.log('t2 PASS sub=', p2.sub)\n\nconsole.log('verify t2 again')\nconst p3 = verify(t2)\nconsole.log('t2-again PASS sub=', p3.sub)\n\nconsole.log('verify t1 again')\nconst p4 = verify(t1)\nconsole.log('t1-again PASS sub=', p4.sub)\n\nObserved output:\n\nverify t1\nt1 PASS sub= userA\n\nverify t2\nt2 PASS sub= userA\n\nverify t2 again\nt2-again PASS sub= userA\n\nverify t1 again\nt1-again PASS sub= userA\n\nThe verifier returns payload from userA when verifying userB.\n\nExpected behavior\n\nCache must not allow returning claims from a different token.\n\nVerification must remain bound to the actual token being validated.\n\nEven if cache collisions occur, the verifier should:\n\nrevalidate signature\n\nre-decode payload\n\nor invalidate cache entry\n\nWhy this is not “just misuse”\n\nThis is not merely a user mistake.\n\nReasons:\n\nfast-jwt explicitly exposes cacheKeyBuilder as an extension point.\n\nThe documentation suggests performance tuning via custom key builders.\n\nNo safeguards exist against collisions.\n\nNo verification binding is performed between:\n\ncached payload\n\noriginal token\n\nThe verifier trusts cache output as authoritative identity.\n\nThis creates a security-sensitive invariant:\n\n\"cache key uniqueness\"\n\nwhich is neither enforced nor validated.\n\nSecurity-critical libraries must assume extension hooks can be misused and implement defensive checks, especially when identity decisions are derived from cached values.\n\nSecurity classification\n\nlogical authorization flaw\n\ncache confusion vulnerability\n\nidentity boundary break\n\nClosest CWE:\n\nCWE-440 — Expected Behavior Violation\n\nSuggested fix (minimal and safe)\n\nBind cache entries to token integrity.\n\nOption A — safest:\n\nStore token hash along with payload and verify match before returning cache.\n\nConceptual patch:\n\nconst tokenHash = hashToken(token)\n\ncache.set(key, { tokenHash, payload })\n\n...\n\nconst entry = cache.get(key)\n\nif (entry && entry.tokenHash === hashToken(token)) {\n return entry.payload\n}\n\nOption B — simpler:\n\nDisable cache usage when custom cacheKeyBuilder is provided.\n\nOption C — defensive:\n\nAlways re-validate signature when cache hit occurs.\n\nNotes\n\nDefault cacheKeyBuilder is safe (hash-based).\n\nIssue appears when custom builders are used — a documented and supported feature.\n\nImpact increases in:\n\nAPI gateways\n\nauth middleware\n\nRBAC layers relying on payload.sub / payload.aud\n\nThis vulnerability is independent from:\n\nRegExp statefulness issue\n\nReDoS claim validation issue\n\nIt is a separate flaw in cache design and trust model.\n\nPoC did on my computer:\n'use strict'\n\nconst fs = require('node:fs')\nconst path = require('node:path')\nconst { createSigner, createVerifier } = require('./src')\n\nfunction nowSec() {\n return Math.floor(Date.now() / 1000)\n}\n\nconst sign = createSigner({ key: 'secret' })\nconst t1 = sign({ sub: 'userA', aud: 'admin', iat: nowSec() })\nconst t2 = sign({ sub: 'userB', aud: 'admin', iat: nowSec() })\n\nfunction badKeyBuilder() {\n return 'aud=admin'\n}\n\nconst verify = createVerifier({\n key: 'secret',\n cache: true,\n cacheTTL: 60000,\n cacheKeyBuilder: badKeyBuilder\n})\n\nfunction run(tok) {\n try {\n const out = verify(tok)\n return { ok: true, sub: out.sub, aud: out.aud }\n } catch (e) {\n return { ok: false, code: e.code || String(e), message: e.message }\n }\n}\n\nconst results = []\nresults.push({ step: 'verify(t1)', token: 't1', result: run(t1) })\nresults.push({ step: 'verify(t2)', token: 't2', result: run(t2) })\nresults.push({ step: 'verify(t2) again', token: 't2', result: run(t2) })\nresults.push({ step: 'verify(t1) again', token: 't1', result: run(t1) })\n\nconst evidence = {\n title: 'fast-jwt cache confusion when cacheKeyBuilder collisions occur',\n environment: {\n node: process.version,\n fastJwt: require('./package.json').version\n },\n config: {\n cache: true,\n cacheTTL: 60000,\n cacheKeyBuilder: \"returns constant key 'aud=admin' (realistic collision pattern)\"\n },\n tokens: {\n t1: { claims: { sub: 'userA', aud: 'admin' }, jwt: t1 },\n t2: { claims: { sub: 'userB', aud: 'admin' }, jwt: t2 }\n },\n observed: results\n}\n\nconst outPath = path.join(process.cwd(), 'evidence-cache-keybuilder-confusion.json')\nfs.writeFileSync(outPath, JSON.stringify(evidence, null, 2))\nconsole.log('Wrote evidence to:', outPath)\n\nfor (const r of results) {\n console.log(r.step, '=>', r.result.ok ? `PASS sub=${r.result.sub}` : `FAIL ${r.result.code}`)\n}\n\nOutput:\nPS C:\\Users\\Franciny Rojas\\Desktop\\crypto-research\\fast-jwt> node poc_cache_keybuilder_confusion_evidence.js\nWrote evidence to: C:\\Users\\Franciny Rojas\\Desktop\\crypto-research\\fast-jwt\\evidence-cache-keybuilder-confusion.json\nverify(t1) => PASS sub=userA\nverify(t2) => PASS sub=userA\nverify(t2) again => PASS sub=userA\nverify(t1) again => PASS sub=userA\nPS C:\\Users\\Franciny Rojas\\Desktop\\crypto-research\\fast-jwt>",
0 commit comments