VYPR
Medium severity5.3NVD Advisory· Published Mar 31, 2026· Updated Apr 2, 2026

CVE-2026-34363

CVE-2026-34363

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.65 and 9.7.0-alpha.9, when multiple clients subscribe to the same class via LiveQuery, the event handlers process each subscriber concurrently using shared mutable objects. The sensitive data filter modifies these shared objects in-place, so when one subscriber's filter removes a protected field, subsequent subscribers may receive the already-filtered object. This can cause protected fields and authentication data to leak to clients that should not see them, or cause clients that should see the data to receive an incomplete object. Additionally, when an afterEvent Cloud Code trigger is registered, one subscriber's trigger modifications can leak to other subscribers through the same shared mutable state. Any Parse Server deployment using LiveQuery with protected fields or afterEvent triggers is affected when multiple clients subscribe to the same class. This issue has been patched in versions 8.6.65 and 9.7.0-alpha.9.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
>= 9.0.0, < 9.7.0-alpha.99.7.0-alpha.9
parse-servernpm
< 8.6.658.6.65

Affected products

9
  • cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*+ 8 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:9.7.0:alpha8:*:*:*:node.js:*:*
    • cpe:2.3:a:parseplatform:parse-server:*:*:*:*:*:node.js:*:*range: <8.6.65

Patches

2
5834e2923459

fix: LiveQuery protected field leak via shared mutable state across concurrent subscribers ([GHSA-m983-v2ff-wq65](https://github.com/parse-community/parse-server/security/advisories/GHSA-m983-v2ff-wq65)) (#10331)

2 files changed · +354 18
  • spec/vulnerabilities.spec.js+328 0 modified
    @@ -3072,6 +3072,334 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
         ]);
       });
     
    +  describe('(GHSA-m983-v2ff-wq65) LiveQuery shared mutable state race across concurrent subscribers', () => {
    +    // Helper: create a LiveQuery client, wait for open, subscribe, wait for subscription ACK
    +    async function createSubscribedClient({ className, masterKey, installationId }) {
    +      const opts = {
    +        applicationId: 'test',
    +        serverURL: 'ws://localhost:8378',
    +        javascriptKey: 'test',
    +      };
    +      if (masterKey) {
    +        opts.masterKey = 'test';
    +      }
    +      if (installationId) {
    +        opts.installationId = installationId;
    +      }
    +      const client = new Parse.LiveQueryClient(opts);
    +      client.open();
    +      const query = new Parse.Query(className);
    +      const sub = client.subscribe(query);
    +      await new Promise(resolve => sub.on('open', resolve));
    +      return { client, sub };
    +    }
    +
    +    async function setupProtectedClass(className) {
    +      const config = Config.get(Parse.applicationId);
    +      const schemaController = await config.database.loadSchema();
    +      await schemaController.addClassIfNotExists(className, {
    +        secretField: { type: 'String' },
    +        publicField: { type: 'String' },
    +      });
    +      await schemaController.updateClass(
    +        className,
    +        {},
    +        {
    +          find: { '*': true },
    +          get: { '*': true },
    +          create: { '*': true },
    +          update: { '*': true },
    +          delete: { '*': true },
    +          addField: {},
    +          protectedFields: { '*': ['secretField'] },
    +        }
    +      );
    +    }
    +
    +    it('should deliver protected fields to master key LiveQuery client', async () => {
    +      const className = 'MasterKeyProtectedClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +
    +      try {
    +        const result = new Promise(resolve => {
    +          masterSub.on('create', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        const obj = new Parse.Object(className);
    +        obj.set('secretField', 'MASTER_VISIBLE');
    +        obj.set('publicField', 'public');
    +        await obj.save(null, { useMasterKey: true });
    +
    +        const received = await result;
    +
    +        // Master key client must see protected fields
    +        expect(received.secretField).toBe('MASTER_VISIBLE');
    +        expect(received.publicField).toBe('public');
    +      } finally {
    +        masterClient.close();
    +      }
    +    });
    +
    +    it('should not leak protected fields to regular client when master key client subscribes concurrently on update', async () => {
    +      const className = 'RaceUpdateClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +      const { client: regularClient, sub: regularSub } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +      });
    +
    +      try {
    +        const obj = new Parse.Object(className);
    +        obj.set('secretField', 'TOP_SECRET');
    +        obj.set('publicField', 'visible');
    +        await obj.save(null, { useMasterKey: true });
    +
    +        const masterResult = new Promise(resolve => {
    +          masterSub.on('update', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +        const regularResult = new Promise(resolve => {
    +          regularSub.on('update', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        await obj.save({ publicField: 'updated' }, { useMasterKey: true });
    +        const [master, regular] = await Promise.all([masterResult, regularResult]);
    +
    +        // Regular client must NOT see the secret field
    +        expect(regular.secretField).toBeUndefined();
    +        expect(regular.publicField).toBe('updated');
    +        // Master client must see the secret field
    +        expect(master.secretField).toBe('TOP_SECRET');
    +        expect(master.publicField).toBe('updated');
    +      } finally {
    +        masterClient.close();
    +        regularClient.close();
    +      }
    +    });
    +
    +    it('should not leak protected fields to regular client when master key client subscribes concurrently on create', async () => {
    +      const className = 'RaceCreateClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +      const { client: regularClient, sub: regularSub } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +      });
    +
    +      try {
    +        const masterResult = new Promise(resolve => {
    +          masterSub.on('create', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +        const regularResult = new Promise(resolve => {
    +          regularSub.on('create', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        const newObj = new Parse.Object(className);
    +        newObj.set('secretField', 'SECRET');
    +        newObj.set('publicField', 'public');
    +        await newObj.save(null, { useMasterKey: true });
    +
    +        const [master, regular] = await Promise.all([masterResult, regularResult]);
    +
    +        expect(regular.secretField).toBeUndefined();
    +        expect(regular.publicField).toBe('public');
    +        expect(master.secretField).toBe('SECRET');
    +        expect(master.publicField).toBe('public');
    +      } finally {
    +        masterClient.close();
    +        regularClient.close();
    +      }
    +    });
    +
    +    it('should not leak protected fields to regular client when master key client subscribes concurrently on delete', async () => {
    +      const className = 'RaceDeleteClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +      const { client: regularClient, sub: regularSub } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +      });
    +
    +      try {
    +        const obj = new Parse.Object(className);
    +        obj.set('secretField', 'SECRET');
    +        obj.set('publicField', 'public');
    +        await obj.save(null, { useMasterKey: true });
    +
    +        const masterResult = new Promise(resolve => {
    +          masterSub.on('delete', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +        const regularResult = new Promise(resolve => {
    +          regularSub.on('delete', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        await obj.destroy({ useMasterKey: true });
    +        const [master, regular] = await Promise.all([masterResult, regularResult]);
    +
    +        expect(regular.secretField).toBeUndefined();
    +        expect(regular.publicField).toBe('public');
    +        expect(master.secretField).toBe('SECRET');
    +        expect(master.publicField).toBe('public');
    +      } finally {
    +        masterClient.close();
    +        regularClient.close();
    +      }
    +    });
    +
    +    it('should not corrupt object when afterEvent trigger modifies res.object for one client', async () => {
    +      const className = 'TriggerRaceClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        startLiveQueryServer: true,
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, req => {
    +        if (req.object) {
    +          req.object.set('injected', `for-${req.installationId}`);
    +        }
    +      });
    +      const config = Config.get(Parse.applicationId);
    +      const schemaController = await config.database.loadSchema();
    +      await schemaController.addClassIfNotExists(className, {
    +        data: { type: 'String' },
    +        injected: { type: 'String' },
    +      });
    +
    +      const { client: client1, sub: sub1 } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +        installationId: 'client-1',
    +      });
    +      const { client: client2, sub: sub2 } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +        installationId: 'client-2',
    +      });
    +
    +      try {
    +        const result1 = new Promise(resolve => {
    +          sub1.on('create', object => {
    +            resolve({ data: object.get('data'), injected: object.get('injected') });
    +          });
    +        });
    +        const result2 = new Promise(resolve => {
    +          sub2.on('create', object => {
    +            resolve({ data: object.get('data'), injected: object.get('injected') });
    +          });
    +        });
    +
    +        const newObj = new Parse.Object(className);
    +        newObj.set('data', 'value');
    +        await newObj.save(null, { useMasterKey: true });
    +
    +        const [r1, r2] = await Promise.all([result1, result2]);
    +
    +        expect(r1.data).toBe('value');
    +        expect(r2.data).toBe('value');
    +        expect(r1.injected).toBe('for-client-1');
    +        expect(r2.injected).toBe('for-client-2');
    +        expect(r1.injected).not.toBe(r2.injected);
    +      } finally {
    +        client1.close();
    +        client2.close();
    +      }
    +    });
    +  });
    +
       describe('(GHSA-pfj7-wv7c-22pr) AuthData subset validation bypass with allowExpiredAuthDataToken', () => {
         let validatorSpy;
     
    
  • src/LiveQuery/ParseLiveQueryServer.ts+26 18 modified
    @@ -206,6 +206,8 @@ class ParseLiveQueryServer {
               continue;
             }
             requestIds.forEach(async requestId => {
    +          // Deep-clone shared object so each concurrent callback works on its own copy
    +          let localDeletedParseObject = JSON.parse(JSON.stringify(deletedParseObject));
               const acl = message.currentParseObject.getACL();
               // Check CLP
               const op = this._getCLPOperation(subscription.query);
    @@ -228,7 +230,7 @@ class ParseLiveQueryServer {
                 res = {
                   event: 'delete',
                   sessionToken: client.sessionToken,
    -              object: deletedParseObject,
    +              object: localDeletedParseObject,
                   clients: this.clients.size,
                   subscriptions: this.subscriptions.size,
                   useMasterKey: client.hasMasterKey,
    @@ -250,9 +252,9 @@ class ParseLiveQueryServer {
                   return;
                 }
                 if (res.object && typeof res.object.toJSON === 'function') {
    -              deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
    +              localDeletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
                 }
    -            res.object = deletedParseObject;
    +            res.object = localDeletedParseObject;
                 await this._filterSensitiveData(
                   classLevelPermissions,
                   res,
    @@ -261,8 +263,7 @@ class ParseLiveQueryServer {
                   op,
                   subscription.query
                 );
    -            deletedParseObject = res.object;
    -            client.pushDelete(requestId, deletedParseObject);
    +            client.pushDelete(requestId, res.object);
               } catch (e) {
                 const error = resolveError(e);
                 Client.pushError(client.parseWebSocket, error.code, error.message, false, requestId);
    @@ -318,6 +319,13 @@ class ParseLiveQueryServer {
               continue;
             }
             requestIds.forEach(async requestId => {
    +          // Deep-clone shared objects so each concurrent callback works on its own copy.
    +          // Without cloning, _filterSensitiveData's in-place field deletion and afterEvent
    +          // trigger modifications corrupt the shared state across concurrent subscribers.
    +          let localCurrentParseObject = JSON.parse(JSON.stringify(currentParseObject));
    +          let localOriginalParseObject = originalParseObject
    +            ? JSON.parse(JSON.stringify(originalParseObject))
    +            : null;
               // Set orignal ParseObject ACL checking promise, if the object does not match
               // subscription, we do not need to check ACL
               let originalACLCheckingPromise;
    @@ -358,8 +366,8 @@ class ParseLiveQueryServer {
                 ]);
                 logger.verbose(
                   'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
    -              originalParseObject,
    -              currentParseObject,
    +              localOriginalParseObject,
    +              localCurrentParseObject,
                   isOriginalSubscriptionMatched,
                   isCurrentSubscriptionMatched,
                   isOriginalMatched,
    @@ -373,7 +381,7 @@ class ParseLiveQueryServer {
                 } else if (isOriginalMatched && !isCurrentMatched) {
                   type = 'leave';
                 } else if (!isOriginalMatched && isCurrentMatched) {
    -              if (originalParseObject) {
    +              if (localOriginalParseObject) {
                     type = 'enter';
                   } else {
                     type = 'create';
    @@ -388,8 +396,8 @@ class ParseLiveQueryServer {
                 res = {
                   event: type,
                   sessionToken: client.sessionToken,
    -              object: currentParseObject,
    -              original: originalParseObject,
    +              object: localCurrentParseObject,
    +              original: localOriginalParseObject,
                   clients: this.clients.size,
                   subscriptions: this.subscriptions.size,
                   useMasterKey: client.hasMasterKey,
    @@ -414,16 +422,16 @@ class ParseLiveQueryServer {
                   return;
                 }
                 if (res.object && typeof res.object.toJSON === 'function') {
    -              currentParseObject = toJSONwithObjects(res.object, res.object.className || className);
    +              localCurrentParseObject = toJSONwithObjects(res.object, res.object.className || className);
                 }
                 if (res.original && typeof res.original.toJSON === 'function') {
    -              originalParseObject = toJSONwithObjects(
    +              localOriginalParseObject = toJSONwithObjects(
                     res.original,
                     res.original.className || className
                   );
                 }
    -            res.object = currentParseObject;
    -            res.original = originalParseObject;
    +            res.object = localCurrentParseObject;
    +            res.original = localOriginalParseObject;
                 await this._filterSensitiveData(
                   classLevelPermissions,
                   res,
    @@ -432,11 +440,9 @@ class ParseLiveQueryServer {
                   op,
                   subscription.query
                 );
    -            currentParseObject = res.object;
    -            originalParseObject = res.original ?? null;
                 const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
                 if (client[functionName]) {
    -              client[functionName](requestId, currentParseObject, originalParseObject);
    +              client[functionName](requestId, res.object, res.original ?? null);
                 }
               } catch (e) {
                 const error = resolveError(e);
    @@ -764,7 +770,9 @@ class ParseLiveQueryServer {
             return;
           }
           let protectedFields = classLevelPermissions?.protectedFields || [];
    -      if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
    +      if (client.hasMasterKey) {
    +        protectedFields = [];
    +      } else if (!Array.isArray(protectedFields)) {
             protectedFields = getDatabaseController(this.config).addProtectedFields(
               classLevelPermissions,
               res.object.className,
    
776c71c3078e

fix: LiveQuery protected field leak via shared mutable state across concurrent subscribers ([GHSA-m983-v2ff-wq65](https://github.com/parse-community/parse-server/security/advisories/GHSA-m983-v2ff-wq65)) (#10330)

2 files changed · +353 18
  • spec/vulnerabilities.spec.js+327 0 modified
    @@ -3404,6 +3404,333 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
         ]);
       });
     
    +  describe('(GHSA-m983-v2ff-wq65) LiveQuery shared mutable state race across concurrent subscribers', () => {
    +    // Helper: create a LiveQuery client, wait for open, subscribe, wait for subscription ACK
    +    async function createSubscribedClient({ className, masterKey, installationId }) {
    +      const opts = {
    +        applicationId: 'test',
    +        serverURL: 'ws://localhost:8378',
    +        javascriptKey: 'test',
    +      };
    +      if (masterKey) {
    +        opts.masterKey = 'test';
    +      }
    +      if (installationId) {
    +        opts.installationId = installationId;
    +      }
    +      const client = new Parse.LiveQueryClient(opts);
    +      client.open();
    +      const query = new Parse.Query(className);
    +      const sub = client.subscribe(query);
    +      await new Promise(resolve => sub.on('open', resolve));
    +      return { client, sub };
    +    }
    +
    +    async function setupProtectedClass(className) {
    +      const config = Config.get(Parse.applicationId);
    +      const schemaController = await config.database.loadSchema();
    +      await schemaController.addClassIfNotExists(className, {
    +        secretField: { type: 'String' },
    +        publicField: { type: 'String' },
    +      });
    +      await schemaController.updateClass(
    +        className,
    +        {},
    +        {
    +          find: { '*': true },
    +          get: { '*': true },
    +          create: { '*': true },
    +          update: { '*': true },
    +          delete: { '*': true },
    +          addField: {},
    +          protectedFields: { '*': ['secretField'] },
    +        }
    +      );
    +    }
    +
    +    it('should deliver protected fields to master key LiveQuery client', async () => {
    +      const className = 'MasterKeyProtectedClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +
    +      try {
    +        const result = new Promise(resolve => {
    +          masterSub.on('create', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        const obj = new Parse.Object(className);
    +        obj.set('secretField', 'MASTER_VISIBLE');
    +        obj.set('publicField', 'public');
    +        await obj.save(null, { useMasterKey: true });
    +
    +        const received = await result;
    +
    +        // Master key client must see protected fields
    +        expect(received.secretField).toBe('MASTER_VISIBLE');
    +        expect(received.publicField).toBe('public');
    +      } finally {
    +        masterClient.close();
    +      }
    +    });
    +
    +    it('should not leak protected fields to regular client when master key client subscribes concurrently on update', async () => {
    +      const className = 'RaceUpdateClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +      const { client: regularClient, sub: regularSub } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +      });
    +
    +      try {
    +        const obj = new Parse.Object(className);
    +        obj.set('secretField', 'TOP_SECRET');
    +        obj.set('publicField', 'visible');
    +        await obj.save(null, { useMasterKey: true });
    +
    +        const masterResult = new Promise(resolve => {
    +          masterSub.on('update', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +        const regularResult = new Promise(resolve => {
    +          regularSub.on('update', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        await obj.save({ publicField: 'updated' }, { useMasterKey: true });
    +        const [master, regular] = await Promise.all([masterResult, regularResult]);
    +        // Regular client must NOT see the secret field
    +        expect(regular.secretField).toBeUndefined();
    +        expect(regular.publicField).toBe('updated');
    +        // Master client must see the secret field
    +        expect(master.secretField).toBe('TOP_SECRET');
    +        expect(master.publicField).toBe('updated');
    +      } finally {
    +        masterClient.close();
    +        regularClient.close();
    +      }
    +    });
    +
    +    it('should not leak protected fields to regular client when master key client subscribes concurrently on create', async () => {
    +      const className = 'RaceCreateClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +      const { client: regularClient, sub: regularSub } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +      });
    +
    +      try {
    +        const masterResult = new Promise(resolve => {
    +          masterSub.on('create', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +        const regularResult = new Promise(resolve => {
    +          regularSub.on('create', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        const newObj = new Parse.Object(className);
    +        newObj.set('secretField', 'SECRET');
    +        newObj.set('publicField', 'public');
    +        await newObj.save(null, { useMasterKey: true });
    +
    +        const [master, regular] = await Promise.all([masterResult, regularResult]);
    +
    +        expect(regular.secretField).toBeUndefined();
    +        expect(regular.publicField).toBe('public');
    +        expect(master.secretField).toBe('SECRET');
    +        expect(master.publicField).toBe('public');
    +      } finally {
    +        masterClient.close();
    +        regularClient.close();
    +      }
    +    });
    +
    +    it('should not leak protected fields to regular client when master key client subscribes concurrently on delete', async () => {
    +      const className = 'RaceDeleteClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        liveQueryServerOptions: {
    +          keyPairs: { masterKey: 'test', javascriptKey: 'test' },
    +        },
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, () => {});
    +      await setupProtectedClass(className);
    +
    +      const { client: masterClient, sub: masterSub } = await createSubscribedClient({
    +        className,
    +        masterKey: true,
    +      });
    +      const { client: regularClient, sub: regularSub } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +      });
    +
    +      try {
    +        const obj = new Parse.Object(className);
    +        obj.set('secretField', 'SECRET');
    +        obj.set('publicField', 'public');
    +        await obj.save(null, { useMasterKey: true });
    +
    +        const masterResult = new Promise(resolve => {
    +          masterSub.on('delete', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +        const regularResult = new Promise(resolve => {
    +          regularSub.on('delete', object => {
    +            resolve({
    +              secretField: object.get('secretField'),
    +              publicField: object.get('publicField'),
    +            });
    +          });
    +        });
    +
    +        await obj.destroy({ useMasterKey: true });
    +        const [master, regular] = await Promise.all([masterResult, regularResult]);
    +
    +        expect(regular.secretField).toBeUndefined();
    +        expect(regular.publicField).toBe('public');
    +        expect(master.secretField).toBe('SECRET');
    +        expect(master.publicField).toBe('public');
    +      } finally {
    +        masterClient.close();
    +        regularClient.close();
    +      }
    +    });
    +
    +    it('should not corrupt object when afterEvent trigger modifies res.object for one client', async () => {
    +      const className = 'TriggerRaceClass';
    +      Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +      await reconfigureServer({
    +        liveQuery: { classNames: [className] },
    +        startLiveQueryServer: true,
    +        verbose: false,
    +        silent: true,
    +      });
    +      Parse.Cloud.afterLiveQueryEvent(className, req => {
    +        if (req.object) {
    +          req.object.set('injected', `for-${req.installationId}`);
    +        }
    +      });
    +      const config = Config.get(Parse.applicationId);
    +      const schemaController = await config.database.loadSchema();
    +      await schemaController.addClassIfNotExists(className, {
    +        data: { type: 'String' },
    +        injected: { type: 'String' },
    +      });
    +
    +      const { client: client1, sub: sub1 } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +        installationId: 'client-1',
    +      });
    +      const { client: client2, sub: sub2 } = await createSubscribedClient({
    +        className,
    +        masterKey: false,
    +        installationId: 'client-2',
    +      });
    +
    +      try {
    +        const result1 = new Promise(resolve => {
    +          sub1.on('create', object => {
    +            resolve({ data: object.get('data'), injected: object.get('injected') });
    +          });
    +        });
    +        const result2 = new Promise(resolve => {
    +          sub2.on('create', object => {
    +            resolve({ data: object.get('data'), injected: object.get('injected') });
    +          });
    +        });
    +
    +        const newObj = new Parse.Object(className);
    +        newObj.set('data', 'value');
    +        await newObj.save(null, { useMasterKey: true });
    +
    +        const [r1, r2] = await Promise.all([result1, result2]);
    +
    +        expect(r1.data).toBe('value');
    +        expect(r2.data).toBe('value');
    +        expect(r1.injected).toBe('for-client-1');
    +        expect(r2.injected).toBe('for-client-2');
    +        expect(r1.injected).not.toBe(r2.injected);
    +      } finally {
    +        client1.close();
    +        client2.close();
    +      }
    +    });
    +  });
    +
       describe('(GHSA-pfj7-wv7c-22pr) AuthData subset validation bypass with allowExpiredAuthDataToken', () => {
         let validatorSpy;
     
    
  • src/LiveQuery/ParseLiveQueryServer.ts+26 18 modified
    @@ -206,6 +206,8 @@ class ParseLiveQueryServer {
               continue;
             }
             requestIds.forEach(async requestId => {
    +          // Deep-clone shared object so each concurrent callback works on its own copy
    +          let localDeletedParseObject = JSON.parse(JSON.stringify(deletedParseObject));
               const acl = message.currentParseObject.getACL();
               // Check CLP
               const op = this._getCLPOperation(subscription.query);
    @@ -228,7 +230,7 @@ class ParseLiveQueryServer {
                 res = {
                   event: 'delete',
                   sessionToken: client.sessionToken,
    -              object: deletedParseObject,
    +              object: localDeletedParseObject,
                   clients: this.clients.size,
                   subscriptions: this.subscriptions.size,
                   useMasterKey: client.hasMasterKey,
    @@ -250,9 +252,9 @@ class ParseLiveQueryServer {
                   return;
                 }
                 if (res.object && typeof res.object.toJSON === 'function') {
    -              deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
    +              localDeletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
                 }
    -            res.object = deletedParseObject;
    +            res.object = localDeletedParseObject;
                 await this._filterSensitiveData(
                   classLevelPermissions,
                   res,
    @@ -261,8 +263,7 @@ class ParseLiveQueryServer {
                   op,
                   subscription.query
                 );
    -            deletedParseObject = res.object;
    -            client.pushDelete(requestId, deletedParseObject);
    +            client.pushDelete(requestId, res.object);
               } catch (e) {
                 const error = resolveError(e);
                 Client.pushError(client.parseWebSocket, error.code, error.message, false, requestId);
    @@ -318,6 +319,13 @@ class ParseLiveQueryServer {
               continue;
             }
             requestIds.forEach(async requestId => {
    +          // Deep-clone shared objects so each concurrent callback works on its own copy.
    +          // Without cloning, _filterSensitiveData's in-place field deletion and afterEvent
    +          // trigger modifications corrupt the shared state across concurrent subscribers.
    +          let localCurrentParseObject = JSON.parse(JSON.stringify(currentParseObject));
    +          let localOriginalParseObject = originalParseObject
    +            ? JSON.parse(JSON.stringify(originalParseObject))
    +            : null;
               // Set orignal ParseObject ACL checking promise, if the object does not match
               // subscription, we do not need to check ACL
               let originalACLCheckingPromise;
    @@ -358,8 +366,8 @@ class ParseLiveQueryServer {
                 ]);
                 logger.verbose(
                   'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
    -              originalParseObject,
    -              currentParseObject,
    +              localOriginalParseObject,
    +              localCurrentParseObject,
                   isOriginalSubscriptionMatched,
                   isCurrentSubscriptionMatched,
                   isOriginalMatched,
    @@ -373,7 +381,7 @@ class ParseLiveQueryServer {
                 } else if (isOriginalMatched && !isCurrentMatched) {
                   type = 'leave';
                 } else if (!isOriginalMatched && isCurrentMatched) {
    -              if (originalParseObject) {
    +              if (localOriginalParseObject) {
                     type = 'enter';
                   } else {
                     type = 'create';
    @@ -388,8 +396,8 @@ class ParseLiveQueryServer {
                 res = {
                   event: type,
                   sessionToken: client.sessionToken,
    -              object: currentParseObject,
    -              original: originalParseObject,
    +              object: localCurrentParseObject,
    +              original: localOriginalParseObject,
                   clients: this.clients.size,
                   subscriptions: this.subscriptions.size,
                   useMasterKey: client.hasMasterKey,
    @@ -414,16 +422,16 @@ class ParseLiveQueryServer {
                   return;
                 }
                 if (res.object && typeof res.object.toJSON === 'function') {
    -              currentParseObject = toJSONwithObjects(res.object, res.object.className || className);
    +              localCurrentParseObject = toJSONwithObjects(res.object, res.object.className || className);
                 }
                 if (res.original && typeof res.original.toJSON === 'function') {
    -              originalParseObject = toJSONwithObjects(
    +              localOriginalParseObject = toJSONwithObjects(
                     res.original,
                     res.original.className || className
                   );
                 }
    -            res.object = currentParseObject;
    -            res.original = originalParseObject;
    +            res.object = localCurrentParseObject;
    +            res.original = localOriginalParseObject;
                 await this._filterSensitiveData(
                   classLevelPermissions,
                   res,
    @@ -432,11 +440,9 @@ class ParseLiveQueryServer {
                   op,
                   subscription.query
                 );
    -            currentParseObject = res.object;
    -            originalParseObject = res.original ?? null;
                 const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
                 if (client[functionName]) {
    -              client[functionName](requestId, currentParseObject, originalParseObject);
    +              client[functionName](requestId, res.object, res.original ?? null);
                 }
               } catch (e) {
                 const error = resolveError(e);
    @@ -764,7 +770,9 @@ class ParseLiveQueryServer {
             return;
           }
           let protectedFields = classLevelPermissions?.protectedFields || [];
    -      if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
    +      if (client.hasMasterKey) {
    +        protectedFields = [];
    +      } else if (!Array.isArray(protectedFields)) {
             protectedFields = getDatabaseController(this.config).addProtectedFields(
               classLevelPermissions,
               res.object.className,
    

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.