Skip to content

Commit dcf2095

Browse files
authored
PLU-354: prevent batched queries in operations (#789)
## Problem We previously disabled batching multiple operations in a single HTTP request. However, we still allow multiple root level fields in a single operation. It was flagged out during VAPT that this might facilitate potential DOS attacks and should only be enabled when strictly necessary. ## Solution Create a custom apollo plugin to detect if there are multiple root level fields. If there are, throw a `BadUserInputError`. ## How to test 1. Run dev server and open http://localhost:3000/graphql to access the sandbox 2. Test with the following query ``` query Batch { h1: healthcheck { version } h2: healthcheck { version } } ``` 3. You should be able to see the relevant error.
1 parent 1460a33 commit dcf2095

File tree

2 files changed

+55
-1
lines changed

2 files changed

+55
-1
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { assert, describe, expect, it } from 'vitest'
2+
3+
import { server } from '../graphql-instance'
4+
5+
describe('GraphQL instance', () => {
6+
describe('Batching within an operation', () => {
7+
it('should not throw an error if a single root fields is present', async () => {
8+
const result = await server.executeOperation({
9+
query: `query HealthCheck { healthcheck { version } }`,
10+
})
11+
assert(result.body.kind === 'single')
12+
expect(result.body.singleResult.errors).toBeUndefined()
13+
})
14+
15+
it('should throw an error if multiple root fields are present', async () => {
16+
const result = await server.executeOperation({
17+
query: `query TwoHealthChecks { h1: healthcheck { version } h2: healthcheck { version } }`,
18+
})
19+
assert(result.body.kind === 'single')
20+
expect(result.body.singleResult.errors[0]).toHaveProperty(
21+
'code',
22+
'BAD_USER_INPUT',
23+
)
24+
})
25+
})
26+
})

packages/backend/src/helpers/graphql-instance.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/dis
44
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
55
import { makeExecutableSchema } from '@graphql-tools/schema'
66
import { RequestHandler } from 'express'
7+
import { Kind, OperationDefinitionNode } from 'graphql/language'
78
import { applyMiddleware } from 'graphql-middleware'
89

910
import appConfig from '@/config/app'
11+
import { BadUserInputError } from '@/errors/graphql-errors'
1012
import { typeDefs } from '@/graphql/__generated__/typeDefs.generated'
1113
import resolvers from '@/graphql/resolvers'
1214
import authentication, { setCurrentUserContext } from '@/helpers/authentication'
@@ -42,22 +44,48 @@ function ApolloServerPluginUserTracer(): ApolloServerPlugin<AuthenticatedContext
4244
}
4345
}
4446

47+
function PreventBatching(): ApolloServerPlugin {
48+
return {
49+
async requestDidStart() {
50+
return {
51+
async didResolveOperation(requestContext) {
52+
const { document } = requestContext
53+
// There should only be one operation definition per document
54+
const queryDefinition = document.definitions.find(
55+
(definition) => definition.kind === Kind.OPERATION_DEFINITION,
56+
) as OperationDefinitionNode | undefined
57+
58+
// Check if there are multiple selections (root fields) in the query
59+
if (queryDefinition?.selectionSet.selections.length > 1) {
60+
throw new BadUserInputError(
61+
'Multiple root fields in a single operation are not allowed.',
62+
)
63+
}
64+
},
65+
}
66+
},
67+
}
68+
}
69+
4570
const schema = makeExecutableSchema({ typeDefs, resolvers })
4671

4772
const schemaWithMiddleware = applyMiddleware(
4873
schema,
4974
authentication.generate(schema),
5075
)
5176

52-
const server = new ApolloServer<UnauthenticatedContext>({
77+
export const server = new ApolloServer<UnauthenticatedContext>({
5378
schema: schemaWithMiddleware,
5479
introspection: appConfig.isDev,
5580
plugins: [
5681
appConfig.isDev
5782
? ApolloServerPluginLandingPageLocalDefault()
5883
: ApolloServerPluginLandingPageDisabled(),
5984
ApolloServerPluginUserTracer(),
85+
PreventBatching(),
6086
],
87+
// We don't want to allow batching within a single HTTP request, this defaults to false
88+
allowBatchedHttpRequests: false,
6189
formatError: (error) => {
6290
logger.error(error)
6391
let errorMessage = error.message

0 commit comments

Comments
 (0)