VYPR
Critical severityNVD Advisory· Published Feb 25, 2026· Updated Feb 26, 2026

Parse Server: Account takeover via JWT algorithm confusion in Google auth adapter

CVE-2026-27804

Description

Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.3 and 9.1.1-alpha.4, an unauthenticated attacker can forge a Google authentication token with alg: "none" to log in as any user linked to a Google account, without knowing their credentials. All deployments with Google authentication enabled are affected. The fix in versions 8.6.3 and 9.1.1-alpha.4 hardcodes the expected RS256 algorithm instead of trusting the JWT header, and replaces the Google adapter's custom key fetcher with jwks-rsa which rejects unknown key IDs. As a workaround, dsable Google authentication until upgrading is possible.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
>= 9.0.0, < 9.3.1-alpha.49.3.1-alpha.4
parse-servernpm
< 8.6.38.6.3

Affected products

1

Patches

2
9b94083accb7

fix: JWT Algorithm Confusion in Google Auth Adapter ([GHSA-4q3h-vp4r-prv2](https://github.com/parse-community/parse-server/security/advisories/GHSA-4q3h-vp4r-prv2)) (#10073)

5 files changed · +148 128
  • spec/AuthenticationAdapters.spec.js+108 19 modified
    @@ -500,19 +500,60 @@ describe('google auth adapter', () => {
         }
       });
     
    -  // it('should throw error if public key used to encode token is not available', async () => {
    -  //   const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
    -  //   try {
    -  //     spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    -
    -  //     await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
    -  //     fail();
    -  //   } catch (e) {
    -  //     expect(e.message).toBe(
    -  //       `Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
    -  //     );
    -  //   }
    -  // });
    +  it('should reject forged alg:none JWT from advisory PoC (GHSA-4q3h-vp4r-prv2)', async () => {
    +    const header = Buffer.from('{"alg":"none","kid":"nonexistent-key","typ":"JWT"}').toString('base64url');
    +    const payload = Buffer.from('{"sub":"the_user_id","iss":"accounts.google.com","aud":"secret","exp":9999999999}').toString('base64url');
    +    const forgedToken = `${header}.${payload}.`;
    +
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +
    +    try {
    +      await google.validateAuthData(
    +        { id: 'the_user_id', id_token: forgedToken },
    +        { clientId: 'secret' }
    +      );
    +      fail('should have rejected forged token');
    +    } catch (e) {
    +      expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
    +    }
    +  });
    +
    +  it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg', async () => {
    +    const fakeClaim = {
    +      iss: 'https://accounts.google.com',
    +      aud: 'secret',
    +      exp: Date.now(),
    +      sub: 'the_user_id',
    +    };
    +    const fakeDecodedToken = { kid: '123', alg: 'ES256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +    spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
    +
    +    await google.validateAuthData(
    +      { id: 'the_user_id', id_token: 'the_token' },
    +      { clientId: 'secret' }
    +    );
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
    +  });
    +
    +  it('should throw error if Google signing key is not found', async () => {
    +    const fakeDecodedToken = { kid: '789', alg: 'RS256' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.rejectWith(new Error('key not found'));
    +
    +    try {
    +      await google.validateAuthData(
    +        { id: 'the_user_id', id_token: 'the_token' },
    +        { clientId: 'secret' }
    +      );
    +      fail('should have thrown');
    +    } catch (e) {
    +      expect(e.message).toBe('Unable to find matching key for Key ID: 789');
    +    }
    +  });
     
       it('(using client id as string) should verify id_token (google.com)', async () => {
         const fakeClaim = {
    @@ -521,8 +562,10 @@ describe('google auth adapter', () => {
           exp: Date.now(),
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         const result = await google.validateAuthData(
    @@ -537,8 +580,10 @@ describe('google auth adapter', () => {
           iss: 'https://not.google.com',
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         try {
    @@ -561,8 +606,10 @@ describe('google auth adapter', () => {
           exp: Date.now(),
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         try {
    @@ -583,8 +630,10 @@ describe('google auth adapter', () => {
           exp: Date.now(),
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         try {
    @@ -897,7 +946,27 @@ describe('apple signin auth adapter', () => {
           { clientId: 'secret' }
         );
         expect(result).toEqual(fakeClaim);
    -    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
    +  });
    +
    +  it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg (GHSA-4q3h-vp4r-prv2)', async () => {
    +    const fakeClaim = {
    +      iss: 'https://appleid.apple.com',
    +      aud: 'secret',
    +      exp: Date.now(),
    +      sub: 'the_user_id',
    +    };
    +    const fakeDecodedToken = { kid: '123', alg: 'none' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +    spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
    +
    +    await apple.validateAuthData(
    +      { id: 'the_user_id', token: 'the_token' },
    +      { clientId: 'secret' }
    +    );
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
       });
     
       it('should not verify invalid id_token', async () => {
    @@ -1236,7 +1305,27 @@ describe('facebook limited auth adapter', () => {
           { clientId: 'secret' }
         );
         expect(result).toEqual(fakeClaim);
    -    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
    +  });
    +
    +  it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg (GHSA-4q3h-vp4r-prv2)', async () => {
    +    const fakeClaim = {
    +      iss: 'https://www.facebook.com',
    +      aud: 'secret',
    +      exp: Date.now(),
    +      sub: 'the_user_id',
    +    };
    +    const fakeDecodedToken = { kid: '123', alg: 'none' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +    spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
    +
    +    await facebook.validateAuthData(
    +      { id: 'the_user_id', token: 'the_token' },
    +      { clientId: 'secret' }
    +    );
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
       });
     
       it('should not verify invalid id_token', async () => {
    
  • spec/index.spec.js+3 1 modified
    @@ -673,8 +673,10 @@ describe('server', () => {
               exp: Date.now(),
               sub: 'the_user_id',
             };
    -        const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +        const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +        const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
             spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +        spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
             spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
             const user = new Parse.User();
             user
    
  • src/Adapters/Auth/apple.js+2 2 modified
    @@ -77,7 +77,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
       }
     
    -  const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
    +  const { kid: keyId } = authUtils.getHeaderFromToken(token);
       const ONE_HOUR_IN_MS = 3600000;
       let jwtClaims;
     
    @@ -89,7 +89,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
     
       try {
         jwtClaims = jwt.verify(token, signingKey, {
    -      algorithms: algorithm,
    +      algorithms: ['RS256'],
           // the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
           audience: clientId,
         });
    
  • src/Adapters/Auth/facebook.js+2 2 modified
    @@ -136,7 +136,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
       }
     
    -  const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
    +  const { kid: keyId } = authUtils.getHeaderFromToken(token);
       const ONE_HOUR_IN_MS = 3600000;
       let jwtClaims;
     
    @@ -148,7 +148,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
     
       try {
         jwtClaims = jwt.verify(token, signingKey, {
    -      algorithms: algorithm,
    +      algorithms: ['RS256'],
           // the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
           audience: clientId,
         });
    
  • src/Adapters/Auth/google.js+33 104 modified
    @@ -4,6 +4,8 @@
      * @class GoogleAdapter
      * @param {Object} options - The adapter configuration options.
      * @param {string} options.clientId - Your Google application Client ID. Required for authentication.
    + * @param {number} [options.cacheMaxEntries] - Maximum number of JWKS cache entries. Default: 5.
    + * @param {number} [options.cacheMaxAge] - Maximum age of JWKS cache entries in ms. Default: 3600000 (1 hour).
      *
      * @description
      * ## Parse Server Configuration
    @@ -21,93 +23,74 @@
      * The adapter requires the following `authData` fields:
      * - **id**: The Google user ID.
      * - **id_token**: The Google ID token.
    - * - **access_token**: The Google access token.
      *
      * ## Auth Payload
      * ### Example Auth Data Payload
      * ```json
      * {
      *   "google": {
      *     "id": "1234567",
    - *     "id_token": "xxxxx.yyyyy.zzzzz",
    - *     "access_token": "abc123def456ghi789"
    + *     "id_token": "xxxxx.yyyyy.zzzzz"
      *   }
      * }
      * ```
      *
      * ## Notes
      * - Ensure your Google Client ID is configured properly in the Parse Server configuration.
    - * - The `id_token` and `access_token` are validated against Google's authentication services.
    + * - The `id_token` is validated against Google's authentication services.
      *
      * @see {@link https://developers.google.com/identity/sign-in/web/backend-auth Google Authentication Documentation}
      */
     
     'use strict';
     
    -// Helper functions for accessing the google API.
     var Parse = require('parse/node').Parse;
     
    -const https = require('https');
    +const jwksClient = require('jwks-rsa');
     const jwt = require('jsonwebtoken');
     const authUtils = require('./utils');
     
     const TOKEN_ISSUER = 'accounts.google.com';
     const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
     
    -let cache = {};
    +const getGoogleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
    +  const client = jwksClient({
    +    jwksUri: 'https://www.googleapis.com/oauth2/v3/certs',
    +    cache: true,
    +    cacheMaxEntries,
    +    cacheMaxAge,
    +  });
     
    -// Retrieve Google Signin Keys (with cache control)
    -function getGoogleKeyByKeyId(keyId) {
    -  if (cache[keyId] && cache.expiresAt > new Date()) {
    -    return cache[keyId];
    +  let key;
    +  try {
    +    key = await authUtils.getSigningKey(client, keyId);
    +  } catch {
    +    throw new Parse.Error(
    +      Parse.Error.OBJECT_NOT_FOUND,
    +      `Unable to find matching key for Key ID: ${keyId}`
    +    );
       }
    +  return key;
    +};
     
    -  return new Promise((resolve, reject) => {
    -    https
    -      .get(`https://www.googleapis.com/oauth2/v3/certs`, res => {
    -        let data = '';
    -        res.on('data', chunk => {
    -          data += chunk.toString('utf8');
    -        });
    -        res.on('end', () => {
    -          const { keys } = JSON.parse(data);
    -          const pems = keys.reduce(
    -            (pems, { n: modulus, e: exposant, kid }) =>
    -              Object.assign(pems, {
    -                [kid]: rsaPublicKeyToPEM(modulus, exposant),
    -              }),
    -            {}
    -          );
    -
    -          if (res.headers['cache-control']) {
    -            var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
    -
    -            if (expire) {
    -              cache = Object.assign({}, pems, {
    -                expiresAt: new Date(new Date().getTime() + Number(expire[1]) * 1000),
    -              });
    -            }
    -          }
    -
    -          resolve(pems[keyId]);
    -        });
    -      })
    -      .on('error', reject);
    -  });
    -}
    -
    -async function verifyIdToken({ id_token: token, id }, { clientId }) {
    +async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) {
       if (!token) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
       }
     
    -  const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
    +  const { kid: keyId } = authUtils.getHeaderFromToken(token);
    +  const ONE_HOUR_IN_MS = 3600000;
       let jwtClaims;
    -  const googleKey = await getGoogleKeyByKeyId(keyId);
    +
    +  cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS;
    +  cacheMaxEntries = cacheMaxEntries || 5;
    +
    +  const googleKey = await getGoogleKeyByKeyId(keyId, cacheMaxEntries, cacheMaxAge);
    +  const signingKey = googleKey.publicKey || googleKey.rsaPublicKey;
     
       try {
    -    jwtClaims = jwt.verify(token, googleKey, {
    -      algorithms: algorithm,
    +    jwtClaims = jwt.verify(token, signingKey, {
    +      algorithms: ['RS256'],
           audience: clientId,
         });
       } catch (exception) {
    @@ -150,57 +133,3 @@ module.exports = {
       validateAppId: validateAppId,
       validateAuthData: validateAuthData,
     };
    -
    -// Helpers functions to convert the RSA certs to PEM (from jwks-rsa)
    -function rsaPublicKeyToPEM(modulusB64, exponentB64) {
    -  const modulus = new Buffer(modulusB64, 'base64');
    -  const exponent = new Buffer(exponentB64, 'base64');
    -  const modulusHex = prepadSigned(modulus.toString('hex'));
    -  const exponentHex = prepadSigned(exponent.toString('hex'));
    -  const modlen = modulusHex.length / 2;
    -  const explen = exponentHex.length / 2;
    -
    -  const encodedModlen = encodeLengthHex(modlen);
    -  const encodedExplen = encodeLengthHex(explen);
    -  const encodedPubkey =
    -    '30' +
    -    encodeLengthHex(modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2) +
    -    '02' +
    -    encodedModlen +
    -    modulusHex +
    -    '02' +
    -    encodedExplen +
    -    exponentHex;
    -
    -  const der = new Buffer(encodedPubkey, 'hex').toString('base64');
    -
    -  let pem = '-----BEGIN RSA PUBLIC KEY-----\n';
    -  pem += `${der.match(/.{1,64}/g).join('\n')}`;
    -  pem += '\n-----END RSA PUBLIC KEY-----\n';
    -  return pem;
    -}
    -
    -function prepadSigned(hexStr) {
    -  const msb = hexStr[0];
    -  if (msb < '0' || msb > '7') {
    -    return `00${hexStr}`;
    -  }
    -  return hexStr;
    -}
    -
    -function toHex(number) {
    -  const nstr = number.toString(16);
    -  if (nstr.length % 2) {
    -    return `0${nstr}`;
    -  }
    -  return nstr;
    -}
    -
    -function encodeLengthHex(n) {
    -  if (n <= 127) {
    -    return toHex(n);
    -  }
    -  const nHex = toHex(n);
    -  const lengthOfLengthByte = 128 + nHex.length / 2;
    -  return toHex(lengthOfLengthByte) + nHex;
    -}
    
9d5942d50e55

fix: JWT Algorithm Confusion in Google Auth Adapter ([GHSA-4q3h-vp4r-prv2](https://github.com/parse-community/parse-server/security/advisories/GHSA-4q3h-vp4r-prv2)) (#10072)

5 files changed · +148 128
  • spec/AuthenticationAdapters.spec.js+108 19 modified
    @@ -500,19 +500,60 @@ describe('google auth adapter', () => {
         }
       });
     
    -  // it('should throw error if public key used to encode token is not available', async () => {
    -  //   const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
    -  //   try {
    -  //     spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    -
    -  //     await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
    -  //     fail();
    -  //   } catch (e) {
    -  //     expect(e.message).toBe(
    -  //       `Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
    -  //     );
    -  //   }
    -  // });
    +  it('should reject forged alg:none JWT from advisory PoC (GHSA-4q3h-vp4r-prv2)', async () => {
    +    const header = Buffer.from('{"alg":"none","kid":"nonexistent-key","typ":"JWT"}').toString('base64url');
    +    const payload = Buffer.from('{"sub":"the_user_id","iss":"accounts.google.com","aud":"secret","exp":9999999999}').toString('base64url');
    +    const forgedToken = `${header}.${payload}.`;
    +
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +
    +    try {
    +      await google.validateAuthData(
    +        { id: 'the_user_id', id_token: forgedToken },
    +        { clientId: 'secret' }
    +      );
    +      fail('should have rejected forged token');
    +    } catch (e) {
    +      expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
    +    }
    +  });
    +
    +  it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg', async () => {
    +    const fakeClaim = {
    +      iss: 'https://accounts.google.com',
    +      aud: 'secret',
    +      exp: Date.now(),
    +      sub: 'the_user_id',
    +    };
    +    const fakeDecodedToken = { kid: '123', alg: 'ES256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +    spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
    +
    +    await google.validateAuthData(
    +      { id: 'the_user_id', id_token: 'the_token' },
    +      { clientId: 'secret' }
    +    );
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
    +  });
    +
    +  it('should throw error if Google signing key is not found', async () => {
    +    const fakeDecodedToken = { kid: '789', alg: 'RS256' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.rejectWith(new Error('key not found'));
    +
    +    try {
    +      await google.validateAuthData(
    +        { id: 'the_user_id', id_token: 'the_token' },
    +        { clientId: 'secret' }
    +      );
    +      fail('should have thrown');
    +    } catch (e) {
    +      expect(e.message).toBe('Unable to find matching key for Key ID: 789');
    +    }
    +  });
     
       it('(using client id as string) should verify id_token (google.com)', async () => {
         const fakeClaim = {
    @@ -521,8 +562,10 @@ describe('google auth adapter', () => {
           exp: Date.now(),
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         const result = await google.validateAuthData(
    @@ -537,8 +580,10 @@ describe('google auth adapter', () => {
           iss: 'https://not.google.com',
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         try {
    @@ -561,8 +606,10 @@ describe('google auth adapter', () => {
           exp: Date.now(),
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         try {
    @@ -583,8 +630,10 @@ describe('google auth adapter', () => {
           exp: Date.now(),
           sub: 'the_user_id',
         };
    -    const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +    const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
         spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
         spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
     
         try {
    @@ -897,7 +946,27 @@ describe('apple signin auth adapter', () => {
           { clientId: 'secret' }
         );
         expect(result).toEqual(fakeClaim);
    -    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
    +  });
    +
    +  it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg (GHSA-4q3h-vp4r-prv2)', async () => {
    +    const fakeClaim = {
    +      iss: 'https://appleid.apple.com',
    +      aud: 'secret',
    +      exp: Date.now(),
    +      sub: 'the_user_id',
    +    };
    +    const fakeDecodedToken = { kid: '123', alg: 'none' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +    spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
    +
    +    await apple.validateAuthData(
    +      { id: 'the_user_id', token: 'the_token' },
    +      { clientId: 'secret' }
    +    );
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
       });
     
       it('should not verify invalid id_token', async () => {
    @@ -1236,7 +1305,27 @@ describe('facebook limited auth adapter', () => {
           { clientId: 'secret' }
         );
         expect(result).toEqual(fakeClaim);
    -    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
    +  });
    +
    +  it('should pass hardcoded RS256 algorithm to jwt.verify, not the JWT header alg (GHSA-4q3h-vp4r-prv2)', async () => {
    +    const fakeClaim = {
    +      iss: 'https://www.facebook.com',
    +      aud: 'secret',
    +      exp: Date.now(),
    +      sub: 'the_user_id',
    +    };
    +    const fakeDecodedToken = { kid: '123', alg: 'none' };
    +    const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
    +    spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +    spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
    +    spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
    +
    +    await facebook.validateAuthData(
    +      { id: 'the_user_id', token: 'the_token' },
    +      { clientId: 'secret' }
    +    );
    +    expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
       });
     
       it('should not verify invalid id_token', async () => {
    
  • spec/index.spec.js+3 1 modified
    @@ -673,8 +673,10 @@ describe('server', () => {
               exp: Date.now(),
               sub: 'the_user_id',
             };
    -        const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
    +        const fakeDecodedToken = { kid: '123', alg: 'RS256' };
    +        const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
             spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
    +        spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
             spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
             const user = new Parse.User();
             user
    
  • src/Adapters/Auth/apple.js+2 2 modified
    @@ -77,7 +77,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
       }
     
    -  const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
    +  const { kid: keyId } = authUtils.getHeaderFromToken(token);
       const ONE_HOUR_IN_MS = 3600000;
       let jwtClaims;
     
    @@ -89,7 +89,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
     
       try {
         jwtClaims = jwt.verify(token, signingKey, {
    -      algorithms: algorithm,
    +      algorithms: ['RS256'],
           // the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
           audience: clientId,
         });
    
  • src/Adapters/Auth/facebook.js+2 2 modified
    @@ -136,7 +136,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
       }
     
    -  const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
    +  const { kid: keyId } = authUtils.getHeaderFromToken(token);
       const ONE_HOUR_IN_MS = 3600000;
       let jwtClaims;
     
    @@ -148,7 +148,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
     
       try {
         jwtClaims = jwt.verify(token, signingKey, {
    -      algorithms: algorithm,
    +      algorithms: ['RS256'],
           // the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
           audience: clientId,
         });
    
  • src/Adapters/Auth/google.js+33 104 modified
    @@ -4,6 +4,8 @@
      * @class GoogleAdapter
      * @param {Object} options - The adapter configuration options.
      * @param {string} options.clientId - Your Google application Client ID. Required for authentication.
    + * @param {number} [options.cacheMaxEntries] - Maximum number of JWKS cache entries. Default: 5.
    + * @param {number} [options.cacheMaxAge] - Maximum age of JWKS cache entries in ms. Default: 3600000 (1 hour).
      *
      * @description
      * ## Parse Server Configuration
    @@ -21,93 +23,74 @@
      * The adapter requires the following `authData` fields:
      * - **id**: The Google user ID.
      * - **id_token**: The Google ID token.
    - * - **access_token**: The Google access token.
      *
      * ## Auth Payload
      * ### Example Auth Data Payload
      * ```json
      * {
      *   "google": {
      *     "id": "1234567",
    - *     "id_token": "xxxxx.yyyyy.zzzzz",
    - *     "access_token": "abc123def456ghi789"
    + *     "id_token": "xxxxx.yyyyy.zzzzz"
      *   }
      * }
      * ```
      *
      * ## Notes
      * - Ensure your Google Client ID is configured properly in the Parse Server configuration.
    - * - The `id_token` and `access_token` are validated against Google's authentication services.
    + * - The `id_token` is validated against Google's authentication services.
      *
      * @see {@link https://developers.google.com/identity/sign-in/web/backend-auth Google Authentication Documentation}
      */
     
     'use strict';
     
    -// Helper functions for accessing the google API.
     var Parse = require('parse/node').Parse;
     
    -const https = require('https');
    +const jwksClient = require('jwks-rsa');
     const jwt = require('jsonwebtoken');
     const authUtils = require('./utils');
     
     const TOKEN_ISSUER = 'accounts.google.com';
     const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
     
    -let cache = {};
    +const getGoogleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
    +  const client = jwksClient({
    +    jwksUri: 'https://www.googleapis.com/oauth2/v3/certs',
    +    cache: true,
    +    cacheMaxEntries,
    +    cacheMaxAge,
    +  });
     
    -// Retrieve Google Signin Keys (with cache control)
    -function getGoogleKeyByKeyId(keyId) {
    -  if (cache[keyId] && cache.expiresAt > new Date()) {
    -    return cache[keyId];
    +  let key;
    +  try {
    +    key = await authUtils.getSigningKey(client, keyId);
    +  } catch {
    +    throw new Parse.Error(
    +      Parse.Error.OBJECT_NOT_FOUND,
    +      `Unable to find matching key for Key ID: ${keyId}`
    +    );
       }
    +  return key;
    +};
     
    -  return new Promise((resolve, reject) => {
    -    https
    -      .get(`https://www.googleapis.com/oauth2/v3/certs`, res => {
    -        let data = '';
    -        res.on('data', chunk => {
    -          data += chunk.toString('utf8');
    -        });
    -        res.on('end', () => {
    -          const { keys } = JSON.parse(data);
    -          const pems = keys.reduce(
    -            (pems, { n: modulus, e: exposant, kid }) =>
    -              Object.assign(pems, {
    -                [kid]: rsaPublicKeyToPEM(modulus, exposant),
    -              }),
    -            {}
    -          );
    -
    -          if (res.headers['cache-control']) {
    -            var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
    -
    -            if (expire) {
    -              cache = Object.assign({}, pems, {
    -                expiresAt: new Date(new Date().getTime() + Number(expire[1]) * 1000),
    -              });
    -            }
    -          }
    -
    -          resolve(pems[keyId]);
    -        });
    -      })
    -      .on('error', reject);
    -  });
    -}
    -
    -async function verifyIdToken({ id_token: token, id }, { clientId }) {
    +async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) {
       if (!token) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
       }
     
    -  const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
    +  const { kid: keyId } = authUtils.getHeaderFromToken(token);
    +  const ONE_HOUR_IN_MS = 3600000;
       let jwtClaims;
    -  const googleKey = await getGoogleKeyByKeyId(keyId);
    +
    +  cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS;
    +  cacheMaxEntries = cacheMaxEntries || 5;
    +
    +  const googleKey = await getGoogleKeyByKeyId(keyId, cacheMaxEntries, cacheMaxAge);
    +  const signingKey = googleKey.publicKey || googleKey.rsaPublicKey;
     
       try {
    -    jwtClaims = jwt.verify(token, googleKey, {
    -      algorithms: algorithm,
    +    jwtClaims = jwt.verify(token, signingKey, {
    +      algorithms: ['RS256'],
           audience: clientId,
         });
       } catch (exception) {
    @@ -150,57 +133,3 @@ module.exports = {
       validateAppId: validateAppId,
       validateAuthData: validateAuthData,
     };
    -
    -// Helpers functions to convert the RSA certs to PEM (from jwks-rsa)
    -function rsaPublicKeyToPEM(modulusB64, exponentB64) {
    -  const modulus = new Buffer(modulusB64, 'base64');
    -  const exponent = new Buffer(exponentB64, 'base64');
    -  const modulusHex = prepadSigned(modulus.toString('hex'));
    -  const exponentHex = prepadSigned(exponent.toString('hex'));
    -  const modlen = modulusHex.length / 2;
    -  const explen = exponentHex.length / 2;
    -
    -  const encodedModlen = encodeLengthHex(modlen);
    -  const encodedExplen = encodeLengthHex(explen);
    -  const encodedPubkey =
    -    '30' +
    -    encodeLengthHex(modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2) +
    -    '02' +
    -    encodedModlen +
    -    modulusHex +
    -    '02' +
    -    encodedExplen +
    -    exponentHex;
    -
    -  const der = new Buffer(encodedPubkey, 'hex').toString('base64');
    -
    -  let pem = '-----BEGIN RSA PUBLIC KEY-----\n';
    -  pem += `${der.match(/.{1,64}/g).join('\n')}`;
    -  pem += '\n-----END RSA PUBLIC KEY-----\n';
    -  return pem;
    -}
    -
    -function prepadSigned(hexStr) {
    -  const msb = hexStr[0];
    -  if (msb < '0' || msb > '7') {
    -    return `00${hexStr}`;
    -  }
    -  return hexStr;
    -}
    -
    -function toHex(number) {
    -  const nstr = number.toString(16);
    -  if (nstr.length % 2) {
    -    return `0${nstr}`;
    -  }
    -  return nstr;
    -}
    -
    -function encodeLengthHex(n) {
    -  if (n <= 127) {
    -    return toHex(n);
    -  }
    -  const nHex = toHex(n);
    -  const lengthOfLengthByte = 128 + nHex.length / 2;
    -  return toHex(lengthOfLengthByte) + nHex;
    -}
    

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

7

News mentions

0

No linked articles in our index yet.