VYPR
Critical severity9.4OSV Advisory· Published Sep 27, 2025· Updated Apr 15, 2026

CVE-2025-59936

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.

PackageAffected versionsPatched versions
get-jwksnpm
< 11.0.211.0.2

Affected products

1

Patches

2
b9350230aa7c

Release v11.0.2 (#366)

https://github.com/nearform/get-jwksoptic-release-automation[bot]Sep 26, 2025via osv
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",
    
1706a177a80a

Merge commit from fork

https://github.com/nearform/get-jwksMatt WSep 26, 2025via ghsa
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

4

News mentions

0

No linked articles in our index yet.