CVE-2025-59936
Description
get-jwks contains fetch utils for JWKS keys. In versions prior to 11.0.2, a vulnerability in get-jwks can lead to cache poisoning in the JWKS key-fetching mechanism. When 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. The vulnerability will work only if the iss validation is done after the use of get-jwks for keys retrieval. This issue has been patched in version 11.0.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
get-jwksnpm | < 11.0.2 | 11.0.2 |
Affected products
1Patches
2b9350230aa7cRelease v11.0.2 (#366)
1 file changed · +1 −1
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "get-jwks", - "version": "11.0.1", + "version": "11.0.2", "description": "Fetch utils for JWKS keys", "main": "src/get-jwks.js", "types": "./src/get-jwks.d.ts",
3 files changed · +74 −2
src/get-jwks.d.ts+1 −0 modified@@ -10,6 +10,7 @@ type JWKSignature = { domain: string; alg: string; kid: string } type JWK = { [key: string]: any; domain: string; alg: string; kid: string } type GetJwks = { + generateCacheKey: (alg: string, kid: string, normalizedDomain: string) => string getPublicKey: (options?: GetPublicKeyOptions) => Promise<string> getJwk: (signature: JWKSignature) => Promise<JWK> getJwksUri: (normalizedDomain: string) => Promise<string>
src/get-jwks.js+8 −1 modified@@ -7,6 +7,7 @@ const { errorCode, GetJwksError } = require('./error') const ONE_MINUTE = 60 * 1000 const FIVE_SECONDS = 5 * 1000 +const CACHE_KEY_DELIMITER = ':' function ensureTrailingSlash(domain) { return domain[domain.length - 1] === '/' ? domain : `${domain}/` @@ -33,6 +34,11 @@ function buildGetJwks(options = {}) { dispose: (value, key) => staleCache.set(key, value), }) + function generateCacheKey(alg, kid, normalizedDomain) { + const encodedKeyParts = [alg, kid, normalizedDomain].map(encodeURIComponent) + return encodedKeyParts.join(CACHE_KEY_DELIMITER) + } + async function getJwksUri(normalizedDomain) { const response = await fetch( `${normalizedDomain}.well-known/openid-configuration`, @@ -73,7 +79,7 @@ function buildGetJwks(options = {}) { return Promise.reject(error) } - const cacheKey = `${alg}:${kid}:${normalizedDomain}` + const cacheKey = generateCacheKey(alg, kid, normalizedDomain) const cachedJwk = cache.get(cacheKey) if (cachedJwk) { @@ -134,6 +140,7 @@ function buildGetJwks(options = {}) { } return { + generateCacheKey, getPublicKey, getJwk, getJwksUri,
test/cache.spec.js+65 −1 modified@@ -16,7 +16,8 @@ test( const alg = localKey.alg const kid = localKey.kid - getJwks.cache.set(`${alg}:${kid}:${domain}`, Promise.resolve(localKey)) + const cacheKey = getJwks.generateCacheKey(alg, kid, domain) + getJwks.cache.set(cacheKey, Promise.resolve(localKey)) const publicKey = await getJwks.getPublicKey({ domain, alg, kid }) const jwk = await getJwks.getJwk({ domain, alg, kid }) @@ -44,3 +45,66 @@ test( t.assert.equal(cache.ttl, 60000) } ) + +test('if cache key is generated with the correct encoding', async t => { + const getJwks = buildGetJwks() + const alg = 'RS256' + const kid = 'KEY_1' + const normalizedDomain = 'https://example.com/' + + const expectedCacheKey = 'RS256:KEY_1:https%3A%2F%2Fexample.com%2F' + + const generatedCacheKey = getJwks.generateCacheKey( + alg, + kid, + normalizedDomain, + ) + + t.assert.equal(generatedCacheKey, expectedCacheKey) +}) + +test('if cache poisoning is prevented', async t => { + const attackerJwk = { + kid: 'legitkey', + alg: 'RS256', + kty: 'RSA', + use: 'sig', + n: 'attacker_modulus_data', + e: 'AQAB' + } + + const legitimateJwk = { + kid: 'legitkey:https://evil.com/?', + alg: 'RS256', + kty: 'RSA', + use: 'sig', + n: 'legitimate_modulus_data', + e: 'AQAB' + } + + nock('https://evil.com/') + .get('/') + .query(true) + .reply(200, { keys: [attackerJwk] }) + + nock('https://legit.com/') + .get('/.well-known/jwks.json') + .reply(200, { keys: [legitimateJwk] }) + + const getJwks = buildGetJwks() + + await getJwks.getJwk({ + domain: 'https://evil.com/?:https://legit.com', + alg: 'RS256', + kid: 'legitkey' + }) + + const legitimateJwkResult = await getJwks.getJwk({ + domain: 'https://legit.com', + alg: 'RS256', + kid: 'legitkey:https://evil.com/?' + }) + + t.assert.ok(legitimateJwkResult) + t.assert.deepEqual(legitimateJwkResult, legitimateJwk) +})
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.