VYPR
Moderate severityNVD Advisory· Published Mar 24, 2026· Updated Mar 25, 2026

Parse Server: Protected field change detection oracle via LiveQuery watch parameter

CVE-2026-33429

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.54 and 9.6.0-alpha.43, an attacker can subscribe to LiveQuery with a watch parameter targeting a protected field. Although the protected field value is properly stripped from event payloads, the presence or absence of update events reveals whether the protected field changed, creating a binary oracle. For boolean protected fields, the timing of change events is equivalent to knowing the field value. This issue has been patched in versions 8.6.54 and 9.6.0-alpha.43.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
>= 9.0.0, < 9.6.0-alpha.439.6.0-alpha.43
parse-servernpm
< 8.6.548.6.54

Affected products

1

Patches

2
0c0a0a5a37ca

fix: Protected field change detection oracle via LiveQuery watch parameter ([GHSA-qpc3-fg4j-8hgm](https://github.com/parse-community/parse-server/security/advisories/GHSA-qpc3-fg4j-8hgm)) (#10253)

2 files changed · +113 1
  • spec/vulnerabilities.spec.js+101 0 modified
    @@ -3650,3 +3650,104 @@ describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforc
         expect(createSpyC).not.toHaveBeenCalled();
       });
     });
    +
    +describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via LiveQuery watch parameter', () => {
    +  const { sleep } = require('../lib/TestUtils');
    +  let obj;
    +
    +  beforeEach(async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['SecretClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +    });
    +    const config = Config.get(Parse.applicationId);
    +    const schemaController = await config.database.loadSchema();
    +    await schemaController.addClassIfNotExists('SecretClass', {
    +      secretObj: { type: 'Object' },
    +      publicField: { type: 'String' },
    +    });
    +    await schemaController.updateClass(
    +      'SecretClass',
    +      {},
    +      {
    +        find: { '*': true },
    +        get: { '*': true },
    +        create: { '*': true },
    +        update: { '*': true },
    +        delete: { '*': true },
    +        addField: {},
    +        protectedFields: { '*': ['secretObj'] },
    +      }
    +    );
    +
    +    obj = new Parse.Object('SecretClass');
    +    obj.set('secretObj', { apiKey: 'SENSITIVE_KEY_123', score: 42 });
    +    obj.set('publicField', 'visible');
    +    await obj.save(null, { useMasterKey: true });
    +  });
    +
    +  afterEach(async () => {
    +    const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
    +    if (client) {
    +      await client.close();
    +    }
    +  });
    +
    +  it('should reject LiveQuery subscription with protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('secretObj');
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with dot-notation on protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('secretObj.apiKey');
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested dot-notation on protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('secretObj.nested.deep.key');
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
    +    );
    +  });
    +
    +  it('should allow LiveQuery subscription with non-protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('publicField');
    +    const subscription = await query.subscribe();
    +    await Promise.all([
    +      new Promise(resolve => {
    +        subscription.on('update', object => {
    +          expect(object.get('secretObj')).toBeUndefined();
    +          expect(object.get('publicField')).toBe('updated');
    +          resolve();
    +        });
    +      }),
    +      obj.save({ publicField: 'updated' }, { useMasterKey: true }),
    +    ]);
    +  });
    +
    +  it('should not deliver update event when only non-watched field changes', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('publicField');
    +    const subscription = await query.subscribe();
    +    const updateSpy = jasmine.createSpy('update');
    +    subscription.on('update', updateSpy);
    +
    +    // Change a field that is NOT in the watch list
    +    obj.set('secretObj', { apiKey: 'ROTATED_KEY', score: 99 });
    +    await obj.save(null, { useMasterKey: true });
    +    await sleep(500);
    +    expect(updateSpy).not.toHaveBeenCalled();
    +  });
    +
    +});
    
  • src/LiveQuery/ParseLiveQueryServer.ts+12 1 modified
    @@ -1050,7 +1050,7 @@ class ParseLiveQueryServer {
             op
           );
     
    -      // Check protected fields in WHERE clause
    +      // Check protected fields in WHERE clause and WATCH parameter
           if (!client.hasMasterKey) {
             const auth = request.user ? { user: request.user, userRoles: [] } : {};
             const protectedFields =
    @@ -1083,6 +1083,17 @@ class ParseLiveQueryServer {
               };
               checkWhere(request.query.where);
             }
    +        if (protectedFields.length > 0 && Array.isArray(request.query.watch)) {
    +          for (const watchField of request.query.watch) {
    +            const rootField = watchField.split('.')[0];
    +            if (protectedFields.includes(watchField) || protectedFields.includes(rootField)) {
    +              throw new Parse.Error(
    +                Parse.Error.OPERATION_FORBIDDEN,
    +                'Permission denied'
    +              );
    +            }
    +          }
    +        }
           }
     
           // Validate regex patterns in the subscription query
    
c62eacaf38de

fix: Protected field change detection oracle via LiveQuery watch parameter ([GHSA-qpc3-fg4j-8hgm](https://github.com/parse-community/parse-server/security/advisories/GHSA-qpc3-fg4j-8hgm)) (#10254)

2 files changed · +112 1
  • spec/vulnerabilities.spec.js+100 0 modified
    @@ -3416,3 +3416,103 @@ describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforc
         expect(createSpyC).not.toHaveBeenCalled();
       });
     });
    +
    +describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via LiveQuery watch parameter', () => {
    +  const { sleep } = require('../lib/TestUtils');
    +  let obj;
    +
    +  beforeEach(async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['SecretClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +    });
    +    const config = Config.get(Parse.applicationId);
    +    const schemaController = await config.database.loadSchema();
    +    await schemaController.addClassIfNotExists('SecretClass', {
    +      secretObj: { type: 'Object' },
    +      publicField: { type: 'String' },
    +    });
    +    await schemaController.updateClass(
    +      'SecretClass',
    +      {},
    +      {
    +        find: { '*': true },
    +        get: { '*': true },
    +        create: { '*': true },
    +        update: { '*': true },
    +        delete: { '*': true },
    +        addField: {},
    +        protectedFields: { '*': ['secretObj'] },
    +      }
    +    );
    +
    +    obj = new Parse.Object('SecretClass');
    +    obj.set('secretObj', { apiKey: 'SENSITIVE_KEY_123', score: 42 });
    +    obj.set('publicField', 'visible');
    +    await obj.save(null, { useMasterKey: true });
    +  });
    +
    +  afterEach(async () => {
    +    const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
    +    if (client) {
    +      await client.close();
    +    }
    +  });
    +
    +  it('should reject LiveQuery subscription with protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('secretObj');
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with dot-notation on protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('secretObj.apiKey');
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested dot-notation on protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('secretObj.nested.deep.key');
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
    +    );
    +  });
    +
    +  it('should allow LiveQuery subscription with non-protected field in watch', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('publicField');
    +    const subscription = await query.subscribe();
    +    await Promise.all([
    +      new Promise(resolve => {
    +        subscription.on('update', object => {
    +          expect(object.get('secretObj')).toBeUndefined();
    +          expect(object.get('publicField')).toBe('updated');
    +          resolve();
    +        });
    +      }),
    +      obj.save({ publicField: 'updated' }, { useMasterKey: true }),
    +    ]);
    +  });
    +
    +  it('should not deliver update event when only non-watched field changes', async () => {
    +    const query = new Parse.Query('SecretClass');
    +    query.watch('publicField');
    +    const subscription = await query.subscribe();
    +    const updateSpy = jasmine.createSpy('update');
    +    subscription.on('update', updateSpy);
    +
    +    // Change a field that is NOT in the watch list
    +    obj.set('secretObj', { apiKey: 'ROTATED_KEY', score: 99 });
    +    await obj.save(null, { useMasterKey: true });
    +    await sleep(500);
    +    expect(updateSpy).not.toHaveBeenCalled();
    +  });
    +});
    
  • src/LiveQuery/ParseLiveQueryServer.ts+12 1 modified
    @@ -1050,7 +1050,7 @@ class ParseLiveQueryServer {
             op
           );
     
    -      // Check protected fields in WHERE clause
    +      // Check protected fields in WHERE clause and WATCH parameter
           if (!client.hasMasterKey) {
             const auth = request.user ? { user: request.user, userRoles: [] } : {};
             const protectedFields =
    @@ -1083,6 +1083,17 @@ class ParseLiveQueryServer {
               };
               checkWhere(request.query.where);
             }
    +        if (protectedFields.length > 0 && Array.isArray(request.query.watch)) {
    +          for (const watchField of request.query.watch) {
    +            const rootField = watchField.split('.')[0];
    +            if (protectedFields.includes(watchField) || protectedFields.includes(rootField)) {
    +              throw new Parse.Error(
    +                Parse.Error.OPERATION_FORBIDDEN,
    +                'Permission denied'
    +              );
    +            }
    +          }
    +        }
           }
     
           // Validate regex patterns in the subscription query
    

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.