From 59e518cdd065ed49e26c4726458ac37b843c73ba Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:34:13 +0000 Subject: [PATCH] test: GHSA-rqxg-pcc9-f79w v9 Add regression tests for subquery breadth and GraphQL where argument breadth to document that request complexity controls work as designed. --- spec/GraphQLQueryComplexity.spec.js | 34 +++++++++++ spec/RequestComplexity.spec.js | 87 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/spec/GraphQLQueryComplexity.spec.js b/spec/GraphQLQueryComplexity.spec.js index 976cc761f4..8b6ba98800 100644 --- a/spec/GraphQLQueryComplexity.spec.js +++ b/spec/GraphQLQueryComplexity.spec.js @@ -178,4 +178,38 @@ describe('graphql query complexity', () => { expect(result.errors).toBeUndefined(); }); }); + + describe('where argument breadth', () => { + it('should enforce depth and field limits regardless of where argument breadth', async () => { + await setupGraphQL({ + requestComplexity: { graphQLDepth: 3, graphQLFields: 200, subqueryDepth: 1 }, + }); + // The GraphQL where argument may contain many OR branches, but the + // complexity check correctly measures the selection set depth/fields, + // not the where variable content. A query exceeding graphQLDepth is + // rejected even when the where argument is simple. + const result = await graphqlRequest(buildDeepQuery()); + expect(result.errors).toBeDefined(); + expect(result.errors[0].message).toMatch( + /GraphQL query depth of \d+ exceeds maximum allowed depth of 3/ + ); + }); + + it('should allow query with wide where argument when selection set is within limits', async () => { + await setupGraphQL({ + requestComplexity: { graphQLDepth: 10, graphQLFields: 200, subqueryDepth: 1 }, + }); + + const obj = new Parse.Object('TestItem'); + obj.set('name', 'test'); + await obj.save(); + + // Wide where with many OR branches — complexity check measures selection + // set depth and field count, not where argument structure + const orBranches = Array.from({ length: 20 }, (_, i) => `{ name: { equalTo: "test${i}" } }`).join(', '); + const query = `{ testItems(where: { OR: [${orBranches}] }) { edges { node { objectId } } } }`; + const result = await graphqlRequest(query); + expect(result.errors).toBeUndefined(); + }); + }); }); diff --git a/spec/RequestComplexity.spec.js b/spec/RequestComplexity.spec.js index 2b9ec60390..aa33a593fa 100644 --- a/spec/RequestComplexity.spec.js +++ b/spec/RequestComplexity.spec.js @@ -242,6 +242,93 @@ describe('request complexity', () => { rest.find(config, auth.nobody(config), '_User', where) ).toBeResolved(); }); + + it('should allow multiple sibling $inQuery at same depth within limit', async () => { + await reconfigureServer({ + requestComplexity: { subqueryDepth: 1 }, + }); + config = Config.get('test'); + // Multiple sibling $inQuery operators in $or, each at depth 1 — within the limit + const where = { + $or: [ + { username: { $inQuery: { className: '_User', where: { username: 'a' } } } }, + { username: { $inQuery: { className: '_User', where: { username: 'b' } } } }, + { username: { $inQuery: { className: '_User', where: { username: 'c' } } } }, + ], + }; + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeResolved(); + }); + + it('should reject sibling $inQuery when nested beyond depth limit', async () => { + await reconfigureServer({ + requestComplexity: { subqueryDepth: 1 }, + }); + config = Config.get('test'); + // Each sibling contains a nested $inQuery at depth 2 — exceeds limit + const where = { + $or: [ + { + username: { + $inQuery: { + className: '_User', + where: { username: { $inQuery: { className: '_User', where: {} } } }, + }, + }, + }, + { + username: { + $inQuery: { + className: '_User', + where: { username: { $inQuery: { className: '_User', where: {} } } }, + }, + }, + }, + ], + }; + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Subquery nesting depth exceeds maximum allowed depth of 1/), + }) + ); + }); + + it('should allow multiple sibling $notInQuery at same depth within limit', async () => { + await reconfigureServer({ + requestComplexity: { subqueryDepth: 1 }, + }); + config = Config.get('test'); + const where = { + $or: [ + { username: { $notInQuery: { className: '_User', where: { username: 'a' } } } }, + { username: { $notInQuery: { className: '_User', where: { username: 'b' } } } }, + { username: { $notInQuery: { className: '_User', where: { username: 'c' } } } }, + ], + }; + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeResolved(); + }); + + it('should allow mixed sibling $inQuery and $notInQuery at same depth within limit', async () => { + await reconfigureServer({ + requestComplexity: { subqueryDepth: 1 }, + }); + config = Config.get('test'); + const where = { + $or: [ + { username: { $inQuery: { className: '_User', where: { username: 'a' } } } }, + { username: { $notInQuery: { className: '_User', where: { username: 'b' } } } }, + { username: { $inQuery: { className: '_User', where: { username: 'c' } } } }, + ], + }; + await expectAsync( + rest.find(config, auth.nobody(config), '_User', where) + ).toBeResolved(); + }); }); describe('query depth', () => {