From f701bcaf2822d968991aa5bd14153e12ffc59135 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:02:52 +0100 Subject: [PATCH 1/3] fix --- spec/RequestComplexity.spec.js | 128 ++++++++++++++++++ spec/vulnerabilities.spec.js | 19 +++ src/Controllers/DatabaseController.js | 23 +++- src/Options/Definitions.js | 6 + src/Options/docs.js | 1 + src/Options/index.js | 3 + .../CheckGroups/CheckGroupServerConfig.js | 2 +- 7 files changed, 174 insertions(+), 8 deletions(-) diff --git a/spec/RequestComplexity.spec.js b/spec/RequestComplexity.spec.js index 6ee159f548..eb4b95319b 100644 --- a/spec/RequestComplexity.spec.js +++ b/spec/RequestComplexity.spec.js @@ -37,6 +37,30 @@ describe('request complexity', () => { return where; } + function buildNestedOrQuery(depth) { + let where = { username: 'test' }; + for (let i = 0; i < depth; i++) { + where = { $or: [where, { username: 'test' }] }; + } + return where; + } + + function buildNestedAndQuery(depth) { + let where = { username: 'test' }; + for (let i = 0; i < depth; i++) { + where = { $and: [where, { username: 'test' }] }; + } + return where; + } + + function buildNestedNorQuery(depth) { + let where = { username: 'test' }; + for (let i = 0; i < depth; i++) { + where = { $nor: [where, { username: 'test' }] }; + } + return where; + } + describe('config validation', () => { it('should accept valid requestComplexity config', async () => { await expectAsync( @@ -45,6 +69,7 @@ describe('request complexity', () => { includeDepth: 10, includeCount: 100, subqueryDepth: 5, + queryDepth: 10, graphQLDepth: 15, graphQLFields: 300, }, @@ -59,6 +84,7 @@ describe('request complexity', () => { includeDepth: -1, includeCount: -1, subqueryDepth: -1, + queryDepth: -1, graphQLDepth: -1, graphQLFields: -1, }, @@ -112,6 +138,7 @@ describe('request complexity', () => { expect(config.requestComplexity.includeDepth).toBe(3); expect(config.requestComplexity.includeCount).toBe(50); expect(config.requestComplexity.subqueryDepth).toBe(5); + expect(config.requestComplexity.queryDepth).toBe(10); expect(config.requestComplexity.graphQLDepth).toBe(50); expect(config.requestComplexity.graphQLFields).toBe(200); }); @@ -123,6 +150,7 @@ describe('request complexity', () => { includeDepth: 5, includeCount: 50, subqueryDepth: 5, + queryDepth: 10, graphQLDepth: 50, graphQLFields: 200, }); @@ -216,6 +244,106 @@ describe('request complexity', () => { }); }); + describe('query depth', () => { + let config; + + beforeEach(async () => { + await reconfigureServer({ + requestComplexity: { queryDepth: 3 }, + }); + config = Config.get('test'); + }); + + it('should allow $or within depth limit', async () => { + const where = buildNestedOrQuery(3); + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeResolved(); + }); + + it('should reject $or exceeding depth limit', async () => { + const where = buildNestedOrQuery(4); + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/), + }) + ); + }); + + it('should reject $and exceeding depth limit', async () => { + const where = buildNestedAndQuery(4); + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/), + }) + ); + }); + + it('should reject $nor exceeding depth limit', async () => { + const where = buildNestedNorQuery(4); + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/), + }) + ); + }); + + it('should reject mixed nested operators exceeding depth limit', async () => { + // $or > $and > $nor > $or = depth 4 + const where = { + $or: [ + { + $and: [ + { + $nor: [ + { $or: [{ username: 'a' }, { username: 'b' }] }, + ], + }, + ], + }, + ], + }; + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/), + }) + ); + }); + + it('should allow with master key even when exceeding limit', async () => { + const where = buildNestedOrQuery(4); + await expectAsync( + rest.find(config, auth.master(config), '_User', where) + ).toBeResolved(); + }); + + it('should allow with maintenance key even when exceeding limit', async () => { + const where = buildNestedOrQuery(4); + await expectAsync( + rest.find(config, auth.maintenance(config), '_User', where) + ).toBeResolved(); + }); + + it('should allow unlimited when queryDepth is -1', async () => { + await reconfigureServer({ + requestComplexity: { queryDepth: -1 }, + }); + config = Config.get('test'); + const where = buildNestedOrQuery(15); + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeResolved(); + }); + }); + describe('include limits', () => { let config; diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index b9e4042761..c5e7808fa8 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2747,3 +2747,22 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na }); }); }); + +describe('(GHSA-9xp9-j92r-p88v) Stack overflow process crash via deeply nested query operators', () => { + it('rejects deeply nested $or query', async () => { + const auth = require('../lib/Auth'); + const rest = require('../lib/rest'); + const config = Config.get('test'); + let where = { username: 'test' }; + for (let i = 0; i < 15; i++) { + where = { $or: [where, { username: 'test' }] }; + } + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/), + }) + ); + }); +}); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 541be63076..59e3b089be 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -113,18 +113,27 @@ const validateQuery = ( query: any, isMaster: boolean, isMaintenance: boolean, - update: boolean + update: boolean, + options: ?ParseServerOptions, + _depth: number = 0 ): void => { if (isMaintenance) { isMaster = true; } + const rc = options?.requestComplexity; + if (!isMaster && rc && rc.queryDepth !== -1 && _depth > rc.queryDepth) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Query condition nesting depth exceeds maximum allowed depth of ${rc.queryDepth}` + ); + } if (query.ACL) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } if (query.$or) { if (query.$or instanceof Array) { - query.$or.forEach(value => validateQuery(value, isMaster, isMaintenance, update)); + query.$or.forEach(value => validateQuery(value, isMaster, isMaintenance, update, options, _depth + 1)); } else { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); } @@ -132,7 +141,7 @@ const validateQuery = ( if (query.$and) { if (query.$and instanceof Array) { - query.$and.forEach(value => validateQuery(value, isMaster, isMaintenance, update)); + query.$and.forEach(value => validateQuery(value, isMaster, isMaintenance, update, options, _depth + 1)); } else { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); } @@ -140,7 +149,7 @@ const validateQuery = ( if (query.$nor) { if (query.$nor instanceof Array && query.$nor.length > 0) { - query.$nor.forEach(value => validateQuery(value, isMaster, isMaintenance, update)); + query.$nor.forEach(value => validateQuery(value, isMaster, isMaintenance, update, options, _depth + 1)); } else { throw new Parse.Error( Parse.Error.INVALID_QUERY, @@ -581,7 +590,7 @@ class DatabaseController { if (acl) { query = addWriteACL(query, acl); } - validateQuery(query, isMaster, false, true); + validateQuery(query, isMaster, false, true, this.options); return schemaController .getOneSchema(className, true) .catch(error => { @@ -829,7 +838,7 @@ class DatabaseController { if (acl) { query = addWriteACL(query, acl); } - validateQuery(query, isMaster, false, false); + validateQuery(query, isMaster, false, false, this.options); return schemaController .getOneSchema(className) .catch(error => { @@ -1340,7 +1349,7 @@ class DatabaseController { query = addReadACL(query, aclGroup); } } - validateQuery(query, isMaster, isMaintenance, false); + validateQuery(query, isMaster, isMaintenance, false, this.options); if (count) { if (!classExists) { return 0; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index aa9ff33e94..26c091c7c2 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -692,6 +692,12 @@ module.exports.RequestComplexityOptions = { action: parsers.numberParser('includeDepth'), default: 5, }, + queryDepth: { + env: 'PARSE_SERVER_REQUEST_COMPLEXITY_QUERY_DEPTH', + help: 'Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `10`.', + action: parsers.numberParser('queryDepth'), + default: 10, + }, subqueryDepth: { env: 'PARSE_SERVER_REQUEST_COMPLEXITY_SUBQUERY_DEPTH', help: 'Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `5`.', diff --git a/src/Options/docs.js b/src/Options/docs.js index 432ff24440..758e7a7329 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -134,6 +134,7 @@ * @property {Number} graphQLFields Maximum number of field selections in a GraphQL query. Set to `-1` to disable. Default is `200`. * @property {Number} includeCount Maximum number of include paths in a single query. Set to `-1` to disable. Default is `50`. * @property {Number} includeDepth Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `5`. + * @property {Number} queryDepth Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `10`. * @property {Number} subqueryDepth Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `5`. */ diff --git a/src/Options/index.js b/src/Options/index.js index 79c46679c2..3e6661da29 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -434,6 +434,9 @@ export interface RequestComplexityOptions { /* Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `5`. :DEFAULT: 5 */ subqueryDepth: ?number; + /* Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `10`. + :DEFAULT: 10 */ + queryDepth: ?number; /* Maximum depth of GraphQL field selections. Set to `-1` to disable. Default is `50`. :ENV: PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_DEPTH :DEFAULT: 50 */ diff --git a/src/Security/CheckGroups/CheckGroupServerConfig.js b/src/Security/CheckGroups/CheckGroupServerConfig.js index 36b327dc91..8f93e51c35 100644 --- a/src/Security/CheckGroups/CheckGroupServerConfig.js +++ b/src/Security/CheckGroups/CheckGroupServerConfig.js @@ -145,7 +145,7 @@ class CheckGroupServerConfig extends CheckGroup { if (!rc) { throw 1; } - const values = [rc.includeDepth, rc.includeCount, rc.subqueryDepth, rc.graphQLDepth, rc.graphQLFields]; + const values = [rc.includeDepth, rc.includeCount, rc.subqueryDepth, rc.queryDepth, rc.graphQLDepth, rc.graphQLFields]; if (values.some(v => v === -1)) { throw 1; } From 1a0ff79714918a4821fc93b5007a5b3a15fca44c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:21:40 +0100 Subject: [PATCH 2/3] default -1 --- spec/RequestComplexity.spec.js | 4 ++-- spec/vulnerabilities.spec.js | 5 ++++- src/Options/Definitions.js | 4 ++-- src/Options/docs.js | 2 +- src/Options/index.js | 4 ++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/spec/RequestComplexity.spec.js b/spec/RequestComplexity.spec.js index eb4b95319b..2765ca02ec 100644 --- a/spec/RequestComplexity.spec.js +++ b/spec/RequestComplexity.spec.js @@ -138,7 +138,7 @@ describe('request complexity', () => { expect(config.requestComplexity.includeDepth).toBe(3); expect(config.requestComplexity.includeCount).toBe(50); expect(config.requestComplexity.subqueryDepth).toBe(5); - expect(config.requestComplexity.queryDepth).toBe(10); + expect(config.requestComplexity.queryDepth).toBe(-1); expect(config.requestComplexity.graphQLDepth).toBe(50); expect(config.requestComplexity.graphQLFields).toBe(200); }); @@ -150,7 +150,7 @@ describe('request complexity', () => { includeDepth: 5, includeCount: 50, subqueryDepth: 5, - queryDepth: 10, + queryDepth: -1, graphQLDepth: 50, graphQLFields: 200, }); diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index c5e7808fa8..8fa3436537 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2749,7 +2749,10 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na }); describe('(GHSA-9xp9-j92r-p88v) Stack overflow process crash via deeply nested query operators', () => { - it('rejects deeply nested $or query', async () => { + it('rejects deeply nested $or query when queryDepth is set', async () => { + await reconfigureServer({ + requestComplexity: { queryDepth: 10 }, + }); const auth = require('../lib/Auth'); const rest = require('../lib/rest'); const config = Config.get('test'); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 26c091c7c2..763192dca3 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -694,9 +694,9 @@ module.exports.RequestComplexityOptions = { }, queryDepth: { env: 'PARSE_SERVER_REQUEST_COMPLEXITY_QUERY_DEPTH', - help: 'Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `10`.', + help: 'Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`.', action: parsers.numberParser('queryDepth'), - default: 10, + default: -1, }, subqueryDepth: { env: 'PARSE_SERVER_REQUEST_COMPLEXITY_SUBQUERY_DEPTH', diff --git a/src/Options/docs.js b/src/Options/docs.js index 758e7a7329..5fb0f334fb 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -134,7 +134,7 @@ * @property {Number} graphQLFields Maximum number of field selections in a GraphQL query. Set to `-1` to disable. Default is `200`. * @property {Number} includeCount Maximum number of include paths in a single query. Set to `-1` to disable. Default is `50`. * @property {Number} includeDepth Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `5`. - * @property {Number} queryDepth Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `10`. + * @property {Number} queryDepth Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`. * @property {Number} subqueryDepth Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `5`. */ diff --git a/src/Options/index.js b/src/Options/index.js index 3e6661da29..9352efe2d6 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -434,8 +434,8 @@ export interface RequestComplexityOptions { /* Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `5`. :DEFAULT: 5 */ subqueryDepth: ?number; - /* Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `10`. - :DEFAULT: 10 */ + /* Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`. + :DEFAULT: -1 */ queryDepth: ?number; /* Maximum depth of GraphQL field selections. Set to `-1` to disable. Default is `50`. :ENV: PARSE_SERVER_REQUEST_COMPLEXITY_GRAPHQL_DEPTH From cef01c34797ca0880ab9b6ac96f601cf607d6cca Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 15 Mar 2026 02:19:45 +0000 Subject: [PATCH 3/3] Update SecurityCheckGroups.spec.js --- spec/SecurityCheckGroups.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/SecurityCheckGroups.spec.js b/spec/SecurityCheckGroups.spec.js index 354fed8b0e..960281bc14 100644 --- a/spec/SecurityCheckGroups.spec.js +++ b/spec/SecurityCheckGroups.spec.js @@ -37,6 +37,7 @@ describe('Security Check Groups', () => { config.mountPlayground = false; config.readOnlyMasterKey = 'someReadOnlyMasterKey'; config.readOnlyMasterKeyIps = ['127.0.0.1', '::1']; + config.requestComplexity = { queryDepth: 10 }; await reconfigureServer(config); const group = new CheckGroupServerConfig(); @@ -66,6 +67,7 @@ describe('Security Check Groups', () => { includeDepth: -1, includeCount: -1, subqueryDepth: -1, + queryDepth: -1, graphQLDepth: -1, graphQLFields: -1, };