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

Parse Server: LiveQuery subscription query depth bypass

CVE-2026-33508

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.56 and 9.6.0-alpha.45, Parse Server's LiveQuery component does not enforce the requestComplexity.queryDepth configuration setting when processing WebSocket subscription requests. An attacker can send a subscription with deeply nested logical operators, causing excessive recursion and CPU consumption that degrades or disrupts service availability. This issue has been patched in versions 8.6.56 and 9.6.0-alpha.45.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
>= 9.0.0, < 9.6.0-alpha.459.6.0-alpha.45
parse-servernpm
< 8.6.568.6.56

Affected products

1

Patches

2
2126fe4e12f9

fix: LiveQuery subscription query depth bypass ([GHSA-6qh5-m6g3-xhq6](https://github.com/parse-community/parse-server/security/advisories/GHSA-6qh5-m6g3-xhq6)) (#10259)

2 files changed · +144 1
  • spec/vulnerabilities.spec.js+116 0 modified
    @@ -3928,3 +3928,119 @@ describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via Live
       });
     
     });
    +
    +describe('(GHSA-6qh5-m6g3-xhq6) LiveQuery query depth DoS via deeply nested subscription', () => {
    +  afterEach(async () => {
    +    const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
    +    if (client) {
    +      await client.close();
    +    }
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested $or when queryDepth is set', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 15; i++) {
    +      where = { $or: [where] };
    +    }
    +    query._where = where;
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      jasmine.objectContaining({
    +        code: Parse.Error.INVALID_QUERY,
    +        message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
    +      })
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested $and when queryDepth is set', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 50; i++) {
    +      where = { $and: [where] };
    +    }
    +    query._where = where;
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      jasmine.objectContaining({
    +        code: Parse.Error.INVALID_QUERY,
    +        message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
    +      })
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested $nor when queryDepth is set', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 50; i++) {
    +      where = { $nor: [where] };
    +    }
    +    query._where = where;
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      jasmine.objectContaining({
    +        code: Parse.Error.INVALID_QUERY,
    +        message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
    +      })
    +    );
    +  });
    +
    +  it('should allow LiveQuery subscription within the depth limit', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 5; i++) {
    +      where = { $or: [where] };
    +    }
    +    query._where = where;
    +    const subscription = await query.subscribe();
    +    expect(subscription).toBeDefined();
    +  });
    +
    +  it('should allow LiveQuery subscription when queryDepth is disabled', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: -1 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 15; i++) {
    +      where = { $or: [where] };
    +    }
    +    query._where = where;
    +    const subscription = await query.subscribe();
    +    expect(subscription).toBeDefined();
    +  });
    +});
    
  • src/LiveQuery/ParseLiveQueryServer.ts+28 1 modified
    @@ -1023,8 +1023,35 @@ class ParseLiveQueryServer {
               return;
             }
           }
    -      // Check CLP for subscribe operation
    +      // Validate query condition depth
           const appConfig = Config.get(this.config.appId);
    +      if (!client.hasMasterKey) {
    +        const rc = appConfig.requestComplexity;
    +        if (rc && rc.queryDepth !== -1) {
    +          const maxDepth = rc.queryDepth;
    +          const checkDepth = (where: any, depth: number) => {
    +            if (depth > maxDepth) {
    +              throw new Parse.Error(
    +                Parse.Error.INVALID_QUERY,
    +                `Query condition nesting depth exceeds maximum allowed depth of ${maxDepth}`
    +              );
    +            }
    +            if (typeof where !== 'object' || where === null) {
    +              return;
    +            }
    +            for (const op of ['$or', '$and', '$nor']) {
    +              if (Array.isArray(where[op])) {
    +                for (const subQuery of where[op]) {
    +                  checkDepth(subQuery, depth + 1);
    +                }
    +              }
    +            }
    +          };
    +          checkDepth(request.query.where, 0);
    +        }
    +      }
    +
    +      // Check CLP for subscribe operation
           const schemaController = await appConfig.database.loadSchema();
           const classLevelPermissions = schemaController.getClassLevelPermissions(className);
           const op = this._getCLPOperation(request.query);
    
060d27053fb0

fix: LiveQuery subscription query depth bypass ([GHSA-6qh5-m6g3-xhq6](https://github.com/parse-community/parse-server/security/advisories/GHSA-6qh5-m6g3-xhq6)) (#10260)

2 files changed · +144 1
  • spec/vulnerabilities.spec.js+116 0 modified
    @@ -3600,3 +3600,119 @@ describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via Live
         expect(updateSpy).not.toHaveBeenCalled();
       });
     });
    +
    +describe('(GHSA-6qh5-m6g3-xhq6) LiveQuery query depth DoS via deeply nested subscription', () => {
    +  afterEach(async () => {
    +    const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
    +    if (client) {
    +      await client.close();
    +    }
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested $or when queryDepth is set', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 15; i++) {
    +      where = { $or: [where] };
    +    }
    +    query._where = where;
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      jasmine.objectContaining({
    +        code: Parse.Error.INVALID_QUERY,
    +        message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
    +      })
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested $and when queryDepth is set', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 50; i++) {
    +      where = { $and: [where] };
    +    }
    +    query._where = where;
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      jasmine.objectContaining({
    +        code: Parse.Error.INVALID_QUERY,
    +        message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
    +      })
    +    );
    +  });
    +
    +  it('should reject LiveQuery subscription with deeply nested $nor when queryDepth is set', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 50; i++) {
    +      where = { $nor: [where] };
    +    }
    +    query._where = where;
    +    await expectAsync(query.subscribe()).toBeRejectedWith(
    +      jasmine.objectContaining({
    +        code: Parse.Error.INVALID_QUERY,
    +        message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
    +      })
    +    );
    +  });
    +
    +  it('should allow LiveQuery subscription within the depth limit', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: 10 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 5; i++) {
    +      where = { $or: [where] };
    +    }
    +    query._where = where;
    +    const subscription = await query.subscribe();
    +    expect(subscription).toBeDefined();
    +  });
    +
    +  it('should allow LiveQuery subscription when queryDepth is disabled', async () => {
    +    Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
    +    await reconfigureServer({
    +      liveQuery: { classNames: ['TestClass'] },
    +      startLiveQueryServer: true,
    +      verbose: false,
    +      silent: true,
    +      requestComplexity: { queryDepth: -1 },
    +    });
    +    const query = new Parse.Query('TestClass');
    +    let where = { field: 'value' };
    +    for (let i = 0; i < 15; i++) {
    +      where = { $or: [where] };
    +    }
    +    query._where = where;
    +    const subscription = await query.subscribe();
    +    expect(subscription).toBeDefined();
    +  });
    +});
    
  • src/LiveQuery/ParseLiveQueryServer.ts+28 1 modified
    @@ -1023,8 +1023,35 @@ class ParseLiveQueryServer {
               return;
             }
           }
    -      // Check CLP for subscribe operation
    +      // Validate query condition depth
           const appConfig = Config.get(this.config.appId);
    +      if (!client.hasMasterKey) {
    +        const rc = appConfig.requestComplexity;
    +        if (rc && rc.queryDepth !== -1) {
    +          const maxDepth = rc.queryDepth;
    +          const checkDepth = (where: any, depth: number) => {
    +            if (depth > maxDepth) {
    +              throw new Parse.Error(
    +                Parse.Error.INVALID_QUERY,
    +                `Query condition nesting depth exceeds maximum allowed depth of ${maxDepth}`
    +              );
    +            }
    +            if (typeof where !== 'object' || where === null) {
    +              return;
    +            }
    +            for (const op of ['$or', '$and', '$nor']) {
    +              if (Array.isArray(where[op])) {
    +                for (const subQuery of where[op]) {
    +                  checkDepth(subQuery, depth + 1);
    +                }
    +              }
    +            }
    +          };
    +          checkDepth(request.query.where, 0);
    +        }
    +      }
    +
    +      // Check CLP for subscribe operation
           const schemaController = await appConfig.database.loadSchema();
           const classLevelPermissions = schemaController.getClassLevelPermissions(className);
           const op = this._getCLPOperation(request.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.