Skip to content

Commit 18ff763

Browse files
committed
fix: support multi document security
1 parent cfd3189 commit 18ff763

File tree

2 files changed

+137
-4
lines changed

2 files changed

+137
-4
lines changed

spec/ParseGraphQLQueryComplexity.spec.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,5 +529,136 @@ describe('ParseGraphQL Query Complexity', () => {
529529
expect(result.data.users).toBeDefined();
530530
});
531531
});
532+
533+
describe('Multi-operation document handling (Security)', () => {
534+
it('should validate the correct operation when multiple operations are in document', async () => {
535+
await reconfigureServer({
536+
maxGraphQLQueryComplexity: {
537+
fields: 4,
538+
},
539+
});
540+
541+
// Document with two operations: one simple, one complex
542+
const query = `
543+
query SimpleQuery {
544+
users {
545+
edges {
546+
node {
547+
objectId
548+
}
549+
}
550+
}
551+
}
552+
553+
query ComplexQuery {
554+
users {
555+
edges {
556+
node {
557+
objectId
558+
username
559+
createdAt
560+
updatedAt
561+
email
562+
}
563+
}
564+
}
565+
}
566+
`;
567+
568+
// SimpleQuery should pass (4 fields: users, edges, node, objectId)
569+
const simpleResponse = await fetch('http://localhost:13378/graphql', {
570+
method: 'POST',
571+
headers: {
572+
'Content-Type': 'application/json',
573+
'X-Parse-Application-Id': 'test',
574+
'X-Parse-Javascript-Key': 'test',
575+
},
576+
body: JSON.stringify({
577+
query,
578+
operationName: 'SimpleQuery'
579+
})
580+
});
581+
const simpleResult = await simpleResponse.json();
582+
expect(simpleResult.data.users).toBeDefined();
583+
584+
// ComplexQuery should fail (8 fields > 4 limit)
585+
const complexResponse = await fetch('http://localhost:13378/graphql', {
586+
method: 'POST',
587+
headers: {
588+
'Content-Type': 'application/json',
589+
'X-Parse-Application-Id': 'test',
590+
'X-Parse-Javascript-Key': 'test',
591+
},
592+
body: JSON.stringify({
593+
query,
594+
operationName: 'ComplexQuery'
595+
})
596+
});
597+
const complexResult = await complexResponse.json();
598+
expect(complexResult.errors).toBeDefined();
599+
expect(complexResult.errors[0].message).toContain('Number of fields selected exceeds maximum allowed');
600+
});
601+
602+
it('should block complex operation even when simple operation is first in document', async () => {
603+
await reconfigureServer({
604+
maxGraphQLQueryComplexity: {
605+
depth: 2,
606+
},
607+
});
608+
609+
// First operation is simple (within limits), second is complex (exceeds limits)
610+
const query = `
611+
query ShallowQuery {
612+
users {
613+
count
614+
}
615+
}
616+
617+
query DeepQuery {
618+
users {
619+
edges {
620+
node {
621+
objectId
622+
username
623+
}
624+
}
625+
}
626+
}
627+
`;
628+
629+
// ShallowQuery should pass (depth 2)
630+
const shallowResponse = await fetch('http://localhost:13378/graphql', {
631+
method: 'POST',
632+
headers: {
633+
'Content-Type': 'application/json',
634+
'X-Parse-Application-Id': 'test',
635+
'X-Parse-Javascript-Key': 'test',
636+
},
637+
body: JSON.stringify({
638+
query,
639+
operationName: 'ShallowQuery'
640+
})
641+
});
642+
const shallowResult = await shallowResponse.json();
643+
expect(shallowResult.data.users).toBeDefined();
644+
645+
// DeepQuery should fail (depth 4 > 2 limit)
646+
const deepResponse = await fetch('http://localhost:13378/graphql', {
647+
method: 'POST',
648+
headers: {
649+
'Content-Type': 'application/json',
650+
'X-Parse-Application-Id': 'test',
651+
'X-Parse-Javascript-Key': 'test',
652+
},
653+
body: JSON.stringify({
654+
query,
655+
operationName: 'DeepQuery'
656+
})
657+
});
658+
const deepResult = await deepResponse.json();
659+
expect(deepResult.errors).toBeDefined();
660+
expect(deepResult.errors[0].message).toContain('Query depth exceeds maximum allowed depth');
661+
});
662+
});
532663
});
533664

src/GraphQL/helpers/queryComplexity.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { GraphQLError, getOperationAST, Kind } from 'graphql';
33
/**
44
* Calculate the maximum depth and fields (field count) of a GraphQL query
55
* @param {DocumentNode} document - The GraphQL document AST
6+
* @param {string} operationName - Optional operation name to select from multi-operation documents
67
* @param {Object} maxLimits - Optional maximum limits for early exit optimization
78
* @param {number} maxLimits.depth - Maximum depth allowed
89
* @param {number} maxLimits.fields - Maximum fields allowed
910
* @returns {{ depth: number, fields: number }} Maximum depth and total fields
1011
*/
11-
function calculateQueryComplexity(document, maxLimits = {}) {
12-
const operationAST = getOperationAST(document);
12+
function calculateQueryComplexity(document, operationName, maxLimits = {}) {
13+
const operationAST = getOperationAST(document, operationName);
1314
if (!operationAST || !operationAST.selectionSet) {
1415
return { depth: 0, fields: 0 };
1516
}
@@ -96,7 +97,7 @@ export function createComplexityValidationPlugin(config) {
9697
return {
9798
requestDidStart: () => ({
9899
didResolveOperation: async (requestContext) => {
99-
const { document } = requestContext;
100+
const { document, operationName } = requestContext;
100101
const auth = requestContext.contextValue?.auth;
101102

102103
// Skip validation for master/maintenance keys
@@ -118,7 +119,8 @@ export function createComplexityValidationPlugin(config) {
118119

119120
// Calculate depth and fields in a single pass for performance
120121
// Pass max limits for early exit optimization - will throw immediately if exceeded
121-
calculateQueryComplexity(document, maxGraphQLQueryComplexity);
122+
// SECURITY: operationName is crucial for multi-operation documents to validate the correct operation
123+
calculateQueryComplexity(document, operationName, maxGraphQLQueryComplexity);
122124
},
123125
}),
124126
};

0 commit comments

Comments
 (0)