VYPR
Medium severity4.4NVD Advisory· Published Mar 31, 2026· Updated Apr 2, 2026

CVE-2026-34224

CVE-2026-34224

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.64 and 9.7.0-alpha.8, an attacker who possesses a valid authentication provider token and a single MFA recovery code or SMS one-time password can create multiple authenticated sessions by sending concurrent login requests via the authData login endpoint. This defeats the single-use guarantee of MFA recovery codes and SMS one-time passwords, allowing session persistence even after the legitimate user revokes detected sessions. This issue has been patched in versions 8.6.64 and 9.7.0-alpha.8.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
>= 9.0.0, < 9.7.0-alpha.89.7.0-alpha.8
parse-servernpm
< 8.6.648.6.64

Affected products

8
  • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*+ 7 more
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha2:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha3:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha4:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha5:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha6:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha7:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:*:*:*:*:*:node.js:*:*range: <8.6.64

Patches

2
e7efbebba398

fix: MFA single-use token bypass via concurrent authData login requests ([GHSA-w73w-g5xw-rwhf](https://github.com/parse-community/parse-server/security/advisories/GHSA-w73w-g5xw-rwhf)) (#10326)

2 files changed · +117 1
  • spec/vulnerabilities.spec.js+85 0 modified
    @@ -4315,6 +4315,91 @@ describe('(GHSA-2299-ghjr-6vjp) MFA recovery code reuse via concurrent requests'
       });
     });
     
    +describe('(GHSA-w73w-g5xw-rwhf) MFA recovery code reuse via concurrent authData-only login', () => {
    +  const mfaHeaders = {
    +    'X-Parse-Application-Id': 'test',
    +    'X-Parse-REST-API-Key': 'rest',
    +    'Content-Type': 'application/json',
    +  };
    +
    +  let fakeProvider;
    +
    +  beforeEach(async () => {
    +    fakeProvider = {
    +      validateAppId: () => Promise.resolve(),
    +      validateAuthData: () => Promise.resolve(),
    +    };
    +    await reconfigureServer({
    +      auth: {
    +        fakeProvider,
    +        mfa: {
    +          enabled: true,
    +          options: ['TOTP'],
    +          algorithm: 'SHA1',
    +          digits: 6,
    +          period: 30,
    +        },
    +      },
    +    });
    +  });
    +
    +  it('rejects concurrent authData-only logins using the same MFA recovery code', async () => {
    +    const OTPAuth = require('otpauth');
    +
    +    // Create user via authData login with fake provider
    +    const user = await Parse.User.logInWith('fakeProvider', {
    +      authData: { id: 'user1', token: 'fakeToken' },
    +    });
    +
    +    // Enable MFA for this user
    +    const secret = new OTPAuth.Secret();
    +    const totp = new OTPAuth.TOTP({
    +      algorithm: 'SHA1',
    +      digits: 6,
    +      period: 30,
    +      secret,
    +    });
    +    const token = totp.generate();
    +    await user.save(
    +      { authData: { mfa: { secret: secret.base32, token } } },
    +      { sessionToken: user.getSessionToken() }
    +    );
    +
    +    // Get recovery codes from stored auth data
    +    await user.fetch({ useMasterKey: true });
    +    const recoveryCode = user.get('authData').mfa.recovery[0];
    +    expect(recoveryCode).toBeDefined();
    +
    +    // Send concurrent authData-only login requests with the same recovery code
    +    const loginWithRecovery = () =>
    +      request({
    +        method: 'POST',
    +        url: 'http://localhost:8378/1/users',
    +        headers: mfaHeaders,
    +        body: JSON.stringify({
    +          authData: {
    +            fakeProvider: { id: 'user1', token: 'fakeToken' },
    +            mfa: { token: recoveryCode },
    +          },
    +        }),
    +      });
    +
    +    const results = await Promise.allSettled(Array(10).fill().map(() => loginWithRecovery()));
    +
    +    const succeeded = results.filter(r => r.status === 'fulfilled');
    +    const failed = results.filter(r => r.status === 'rejected');
    +
    +    // Exactly one request should succeed; all others should fail
    +    expect(succeeded.length).toBe(1);
    +    expect(failed.length).toBe(9);
    +
    +    // Verify the recovery code has been consumed
    +    await user.fetch({ useMasterKey: true });
    +    const remainingRecovery = user.get('authData').mfa.recovery;
    +    expect(remainingRecovery).not.toContain(recoveryCode);
    +  });
    +});
    +
     describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field names in PostgreSQL adapter', () => {
       const headers = {
         'Content-Type': 'application/json',
    
  • src/RestWrite.js+32 1 modified
    @@ -663,6 +663,15 @@ RestWrite.prototype.handleAuthData = async function (authData) {
           // We are supposed to have a response only on LOGIN with authData, so we skip those
           // If we're not logging in, but just updating the current user, we can safely skip that part
           if (this.response) {
    +        // Capture original authData before mutating userResult via the response reference
    +        const originalAuthData = userResult?.authData
    +          ? Object.fromEntries(
    +            Object.entries(userResult.authData).map(([k, v]) =>
    +              [k, v && typeof v === 'object' ? { ...v } : v]
    +            )
    +          )
    +          : undefined;
    +
             // Assign the new authData in the response
             Object.keys(mutatedAuthData).forEach(provider => {
               this.response.response.authData[provider] = mutatedAuthData[provider];
    @@ -673,14 +682,36 @@ RestWrite.prototype.handleAuthData = async function (authData) {
             // uses the `doNotSave` option. Just update the authData part
             // Then we're good for the user, early exit of sorts
             if (Object.keys(this.data.authData).length) {
    +          const query = { objectId: this.data.objectId };
    +          // Optimistic locking: include the original array fields in the WHERE clause
    +          // for providers whose data is being updated. This prevents concurrent requests
    +          // from both succeeding when consuming single-use tokens (e.g. MFA recovery codes).
    +          if (originalAuthData) {
    +            for (const provider of Object.keys(this.data.authData)) {
    +              const original = originalAuthData[provider];
    +              if (original && typeof original === 'object') {
    +                for (const [field, value] of Object.entries(original)) {
    +                  if (
    +                    Array.isArray(value) &&
    +                    JSON.stringify(value) !== JSON.stringify(this.data.authData[provider]?.[field])
    +                  ) {
    +                    query[`authData.${provider}.${field}`] = value;
    +                  }
    +                }
    +              }
    +            }
    +          }
               try {
                 await this.config.database.update(
                   this.className,
    -              { objectId: this.data.objectId },
    +              query,
                   { authData: this.data.authData },
                   {}
                 );
               } catch (error) {
    +            if (error.code === Parse.Error.OBJECT_NOT_FOUND) {
    +              throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid auth data');
    +            }
                 this._throwIfAuthDataDuplicate(error);
                 throw error;
               }
    
661f160edac8

fix: MFA single-use token bypass via concurrent authData login requests ([GHSA-w73w-g5xw-rwhf](https://github.com/parse-community/parse-server/security/advisories/GHSA-w73w-g5xw-rwhf)) (#10327)

2 files changed · +117 1
  • spec/vulnerabilities.spec.js+85 0 modified
    @@ -4040,6 +4040,91 @@ describe('(GHSA-2299-ghjr-6vjp) MFA recovery code reuse via concurrent requests'
       });
     });
     
    +describe('(GHSA-w73w-g5xw-rwhf) MFA recovery code reuse via concurrent authData-only login', () => {
    +  const mfaHeaders = {
    +    'X-Parse-Application-Id': 'test',
    +    'X-Parse-REST-API-Key': 'rest',
    +    'Content-Type': 'application/json',
    +  };
    +
    +  let fakeProvider;
    +
    +  beforeEach(async () => {
    +    fakeProvider = {
    +      validateAppId: () => Promise.resolve(),
    +      validateAuthData: () => Promise.resolve(),
    +    };
    +    await reconfigureServer({
    +      auth: {
    +        fakeProvider,
    +        mfa: {
    +          enabled: true,
    +          options: ['TOTP'],
    +          algorithm: 'SHA1',
    +          digits: 6,
    +          period: 30,
    +        },
    +      },
    +    });
    +  });
    +
    +  it('rejects concurrent authData-only logins using the same MFA recovery code', async () => {
    +    const OTPAuth = require('otpauth');
    +
    +    // Create user via authData login with fake provider
    +    const user = await Parse.User.logInWith('fakeProvider', {
    +      authData: { id: 'user1', token: 'fakeToken' },
    +    });
    +
    +    // Enable MFA for this user
    +    const secret = new OTPAuth.Secret();
    +    const totp = new OTPAuth.TOTP({
    +      algorithm: 'SHA1',
    +      digits: 6,
    +      period: 30,
    +      secret,
    +    });
    +    const token = totp.generate();
    +    await user.save(
    +      { authData: { mfa: { secret: secret.base32, token } } },
    +      { sessionToken: user.getSessionToken() }
    +    );
    +
    +    // Get recovery codes from stored auth data
    +    await user.fetch({ useMasterKey: true });
    +    const recoveryCode = user.get('authData').mfa.recovery[0];
    +    expect(recoveryCode).toBeDefined();
    +
    +    // Send concurrent authData-only login requests with the same recovery code
    +    const loginWithRecovery = () =>
    +      request({
    +        method: 'POST',
    +        url: 'http://localhost:8378/1/users',
    +        headers: mfaHeaders,
    +        body: JSON.stringify({
    +          authData: {
    +            fakeProvider: { id: 'user1', token: 'fakeToken' },
    +            mfa: { token: recoveryCode },
    +          },
    +        }),
    +      });
    +
    +    const results = await Promise.allSettled(Array(10).fill().map(() => loginWithRecovery()));
    +
    +    const succeeded = results.filter(r => r.status === 'fulfilled');
    +    const failed = results.filter(r => r.status === 'rejected');
    +
    +    // Exactly one request should succeed; all others should fail
    +    expect(succeeded.length).toBe(1);
    +    expect(failed.length).toBe(9);
    +
    +    // Verify the recovery code has been consumed
    +    await user.fetch({ useMasterKey: true });
    +    const remainingRecovery = user.get('authData').mfa.recovery;
    +    expect(remainingRecovery).not.toContain(recoveryCode);
    +  });
    +});
    +
     describe('(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context', () => {
       const headers = {
         'X-Parse-Application-Id': 'test',
    
  • src/RestWrite.js+32 1 modified
    @@ -650,6 +650,15 @@ RestWrite.prototype.handleAuthData = async function (authData) {
           // We are supposed to have a response only on LOGIN with authData, so we skip those
           // If we're not logging in, but just updating the current user, we can safely skip that part
           if (this.response) {
    +        // Capture original authData before mutating userResult via the response reference
    +        const originalAuthData = userResult?.authData
    +          ? Object.fromEntries(
    +            Object.entries(userResult.authData).map(([k, v]) =>
    +              [k, v && typeof v === 'object' ? { ...v } : v]
    +            )
    +          )
    +          : undefined;
    +
             // Assign the new authData in the response
             Object.keys(mutatedAuthData).forEach(provider => {
               this.response.response.authData[provider] = mutatedAuthData[provider];
    @@ -660,14 +669,36 @@ RestWrite.prototype.handleAuthData = async function (authData) {
             // uses the `doNotSave` option. Just update the authData part
             // Then we're good for the user, early exit of sorts
             if (Object.keys(this.data.authData).length) {
    +          const query = { objectId: this.data.objectId };
    +          // Optimistic locking: include the original array fields in the WHERE clause
    +          // for providers whose data is being updated. This prevents concurrent requests
    +          // from both succeeding when consuming single-use tokens (e.g. MFA recovery codes).
    +          if (originalAuthData) {
    +            for (const provider of Object.keys(this.data.authData)) {
    +              const original = originalAuthData[provider];
    +              if (original && typeof original === 'object') {
    +                for (const [field, value] of Object.entries(original)) {
    +                  if (
    +                    Array.isArray(value) &&
    +                    JSON.stringify(value) !== JSON.stringify(this.data.authData[provider]?.[field])
    +                  ) {
    +                    query[`authData.${provider}.${field}`] = value;
    +                  }
    +                }
    +              }
    +            }
    +          }
               try {
                 await this.config.database.update(
                   this.className,
    -              { objectId: this.data.objectId },
    +              query,
                   { authData: this.data.authData },
                   {}
                 );
               } catch (error) {
    +            if (error.code === Parse.Error.OBJECT_NOT_FOUND) {
    +              throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid auth data');
    +            }
                 this._throwIfAuthDataDuplicate(error);
                 throw error;
               }
    

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.