Skip to content

Commit 0ae9c25

Browse files
authored
fix: Denial-of-service via unbounded query complexity in REST and GraphQL API ([GHSA-cmj3-wx7h-ffvg](GHSA-cmj3-wx7h-ffvg)) (#10130)
1 parent 3ed96d3 commit 0ae9c25

14 files changed

+799
-5
lines changed

resources/buildConfigDefinitions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const nestedOptionTypes = [
2222
'PagesOptions',
2323
'PagesRoute',
2424
'PasswordPolicyOptions',
25+
'RequestComplexityOptions',
2526
'SecurityOptions',
2627
'SchemaOptions',
2728
'LogLevels',
@@ -46,6 +47,7 @@ const nestedOptionEnvPrefix = {
4647
ParseServerOptions: 'PARSE_SERVER_',
4748
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
4849
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
50+
RequestComplexityOptions: 'PARSE_SERVER_REQUEST_COMPLEXITY_',
4951
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
5052
SecurityOptions: 'PARSE_SERVER_SECURITY_',
5153
};
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
'use strict';
2+
3+
const http = require('http');
4+
const express = require('express');
5+
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
6+
require('./helper');
7+
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
8+
9+
describe('graphql query complexity', () => {
10+
let httpServer;
11+
let graphQLServer;
12+
const headers = {
13+
'X-Parse-Application-Id': 'test',
14+
'X-Parse-Javascript-Key': 'test',
15+
'Content-Type': 'application/json',
16+
};
17+
18+
async function setupGraphQL(serverOptions = {}) {
19+
if (httpServer) {
20+
await new Promise(resolve => httpServer.close(resolve));
21+
}
22+
const server = await reconfigureServer(serverOptions);
23+
const expressApp = express();
24+
httpServer = http.createServer(expressApp);
25+
expressApp.use('/parse', server.app);
26+
graphQLServer = new ParseGraphQLServer(server, {
27+
graphQLPath: '/graphql',
28+
});
29+
graphQLServer.applyGraphQL(expressApp);
30+
await new Promise(resolve => httpServer.listen({ port: 13378 }, resolve));
31+
}
32+
33+
async function graphqlRequest(query, requestHeaders = headers) {
34+
const response = await fetch('http://localhost:13378/graphql', {
35+
method: 'POST',
36+
headers: requestHeaders,
37+
body: JSON.stringify({ query }),
38+
});
39+
return response.json();
40+
}
41+
42+
// Returns a query with depth 4: users(1) > edges(2) > node(3) > objectId(4)
43+
function buildDeepQuery() {
44+
return '{ users { edges { node { objectId } } } }';
45+
}
46+
47+
function buildWideQuery(fieldCount) {
48+
const fields = Array.from({ length: fieldCount }, (_, i) => `field${i}: objectId`).join('\n ');
49+
return `{ users { edges { node { ${fields} } } } }`;
50+
}
51+
52+
afterEach(async () => {
53+
if (httpServer) {
54+
await new Promise(resolve => httpServer.close(resolve));
55+
httpServer = null;
56+
}
57+
});
58+
59+
describe('depth limit', () => {
60+
it('should reject query exceeding depth limit', async () => {
61+
await setupGraphQL({
62+
requestComplexity: { graphQLDepth: 3 },
63+
});
64+
const result = await graphqlRequest(buildDeepQuery());
65+
expect(result.errors).toBeDefined();
66+
expect(result.errors[0].message).toMatch(
67+
/GraphQL query depth of \d+ exceeds maximum allowed depth of 3/
68+
);
69+
});
70+
71+
it('should allow query within depth limit', async () => {
72+
await setupGraphQL({
73+
requestComplexity: { graphQLDepth: 10 },
74+
});
75+
const result = await graphqlRequest(buildDeepQuery());
76+
expect(result.errors).toBeUndefined();
77+
});
78+
79+
it('should allow deep query with master key', async () => {
80+
await setupGraphQL({
81+
requestComplexity: { graphQLDepth: 3 },
82+
});
83+
const result = await graphqlRequest(buildDeepQuery(), {
84+
...headers,
85+
'X-Parse-Master-Key': 'test',
86+
});
87+
expect(result.errors).toBeUndefined();
88+
});
89+
90+
it('should allow unlimited depth when graphQLDepth is -1', async () => {
91+
await setupGraphQL({
92+
requestComplexity: { graphQLDepth: -1 },
93+
});
94+
const result = await graphqlRequest(buildDeepQuery());
95+
expect(result.errors).toBeUndefined();
96+
});
97+
});
98+
99+
describe('fields limit', () => {
100+
it('should reject query exceeding fields limit', async () => {
101+
await setupGraphQL({
102+
requestComplexity: { graphQLFields: 5 },
103+
});
104+
const result = await graphqlRequest(buildWideQuery(10));
105+
expect(result.errors).toBeDefined();
106+
expect(result.errors[0].message).toMatch(
107+
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(5\)/
108+
);
109+
});
110+
111+
it('should allow query within fields limit', async () => {
112+
await setupGraphQL({
113+
requestComplexity: { graphQLFields: 200 },
114+
});
115+
const result = await graphqlRequest(buildDeepQuery());
116+
expect(result.errors).toBeUndefined();
117+
});
118+
119+
it('should allow wide query with master key', async () => {
120+
await setupGraphQL({
121+
requestComplexity: { graphQLFields: 5 },
122+
});
123+
const result = await graphqlRequest(buildWideQuery(10), {
124+
...headers,
125+
'X-Parse-Master-Key': 'test',
126+
});
127+
expect(result.errors).toBeUndefined();
128+
});
129+
130+
it('should count fragment fields at each spread location', async () => {
131+
// With correct counting: 2 aliases (2) + 2×edges (2) + 2×node (2) + 2×objectId from fragment (2) = 8
132+
// With incorrect counting (fragment once): 2 + 2 + 2 + 1 = 7
133+
// Set limit to 7 so incorrect counting passes but correct counting rejects
134+
await setupGraphQL({
135+
requestComplexity: { graphQLFields: 7 },
136+
});
137+
const result = await graphqlRequest(`
138+
fragment UserFields on User { objectId }
139+
{
140+
a1: users { edges { node { ...UserFields } } }
141+
a2: users { edges { node { ...UserFields } } }
142+
}
143+
`);
144+
expect(result.errors).toBeDefined();
145+
expect(result.errors[0].message).toMatch(
146+
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(7\)/
147+
);
148+
});
149+
150+
it('should count inline fragment fields toward depth and field limits', async () => {
151+
await setupGraphQL({
152+
requestComplexity: { graphQLFields: 3 },
153+
});
154+
// Inline fragment adds fields without increasing depth:
155+
// users(1) > edges(2) > ... on UserConnection { edges(3) > node(4) }
156+
const result = await graphqlRequest(`{
157+
users {
158+
edges {
159+
... on UserEdge {
160+
node {
161+
objectId
162+
}
163+
}
164+
}
165+
}
166+
}`);
167+
expect(result.errors).toBeDefined();
168+
expect(result.errors[0].message).toMatch(
169+
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(3\)/
170+
);
171+
});
172+
173+
it('should allow unlimited fields when graphQLFields is -1', async () => {
174+
await setupGraphQL({
175+
requestComplexity: { graphQLFields: -1 },
176+
});
177+
const result = await graphqlRequest(buildWideQuery(50));
178+
expect(result.errors).toBeUndefined();
179+
});
180+
});
181+
});

spec/ParseGraphQLServer.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9554,6 +9554,12 @@ describe('ParseGraphQLServer', () => {
95549554
});
95559555

95569556
it_only_db('mongo')('should support deep nested creation', async () => {
9557+
parseServer = await global.reconfigureServer({
9558+
maintenanceKey: 'test2',
9559+
maxUploadSize: '1kb',
9560+
requestComplexity: { includeDepth: 10 },
9561+
});
9562+
await createGQLFromParseServer(parseServer);
95579563
const team = new Parse.Object('Team');
95589564
team.set('name', 'imATeam1');
95599565
await team.save();

0 commit comments

Comments
 (0)