In this post, I'm going to explain what is GraphQL batching attack, and how you can defend from it.
GraphQL can send multiple queries with a single request, and this can be open to abuse. Consider this mutation where we are trying different password/username combinations.
mutation {
login(pass: 1111, username: "ivan")
second: login(pass: 2222, username: "ivan")
third: login(pass: 3333, username: "ivan")
fourth: login(pass: 4444, username: "ivan")
}
This is effectively the same query (login
) that is aliased to different names.
Or this query, which can easily DoS your server.
query {
getUsers(first: 1000)
second: getUsers(first: 2000)
third: getUsers(first: 3000)
fourth: getUsers(first: 4000)
}
There are a couple of techniques that can be used to prevent this kind of problem one of them is GraphQL Query Complexity Analysis which is, as the name suggests, very complex to implement correctly. It requires analysis of how the graphql API is used, and what queries and mutations are most often called. If you get this wrong, there is a danger of the server denying perfectly valid queries.
The second solution that can somewhat eliminate this problem is implementing grapql dataLoader
(https://github.com/graphql/dataloader) which is also tricky to get right, and it will require you to change your resolvers.
The third solution which I will present here is to simply disable duplicate queries and mutations.
How it works
While the alias
functionality cannot be directly disabled (at least not in the current Grahpql JS implementation), we need to analyze the query that is coming to the server and if it contains duplicate queries and mutations, simply deny the request.
To deny the request we need to hook in the validation
phase of the GraphQL server. The validation phase is the phase when the request is received by the server, but before it is executed, at that point we can decide if we want to proceed with the execution of the request, or immediately return to the client with the reason why the request has been denied.
For this, we are going to use GraphQL No Alias library.
There are two ways to use this library:
- Using the
@noAlias
directive in theschema
- Using the configuration options (better performance)
Using the directive
There are two parts, a directive
that needs to be added to the schema
, and a validation function that needs to be added to the GraphQL validationRules
array.
In the next example, we are going to start implementing the @noAlias
directive by limiting all
mutations to only one of each (by specifying the directive directly on the mutation type), and we are going to limit query hello
to maximum 2 calls in the same request. For the actual GraphQL server we are going to use express-graphql
but the directive should work with any server implemented in javascript.
In the upcoming examples I'm going to use express-graphql
as the graphql server, simply because it is easiest to setup, however useage with any other server is the same.
const express = require('express')
const { graphqlHTTP } = require('express-graphql')
const { buildSchema } = require('graphql')
const {createValidation} = require('graphql-no-alias')
// get the validation function and type definition of the declaration
const { typeDefs, validation } = createValidation()
//add type defintion to schema
const schema = buildSchema(`
${typeDefs}
type Query {
hello: String @noAlias(allow:2)
}
type Mutation @noAlias {
login(username:String, password:String):String
getUsers(startingId:String):String
}
`)
const app = express()
app.use(
'/graphql',
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
validationRules: [validation] //add the validation function
})
)
app.listen(4000)
Now if you send a query like this:
query {
hello
hello_2: hello
}
It will pass however, this query will not (because the maximum allowed calls for query hello
is 2 calls)
query {
hello
hello_2: hello
hello_3: hello
}
And for the mutation:
mutation {
login(pass: 1111, username: "ivan")
second_login: login(pass: 2222, username: "ivan")
}
This will fail because you can't have any duplicate mutations (@noAlias
directive is set directly on the Mutation
type, with no value, which means that the default value of 1 will be used.
And that's it, that is all it takes to manipulate the number of queries and mutations in GraphQL requests.
Next, we are going to look at using the graphql-no-alias
validation imperatively.
Imperative configuration
There is another way to use graphql-no-alias
validation directive, and that is with the imperative configuration.
When using imperative configuration there is no need for type definition and schema modification, this also results in better performance since the schema
is not analyzed (not looking for directives). All we need to do is to create a simple Javascript object with appropriate keys and pass that object to the createValidation
function.
const permissions = {
Query: {
'*': 2, // default value for all queries
getAnotherUser: 5 // custom value for specific query
},
Mutation: {
'*': 1 //default value for all mutations
}
}
const { validation } = createValidation({ permissions })
const schema = buildSchema(/* GraphQL */ `
type Query {
getUser: User
getAnotherUser: User
}
type User {
name: String
}
`)
const app = express()
app.use(
'/graphql',
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
validationRules: [validation] //add the validation function
})
)
That's it, that is all it takes to disable multiple identical queries and mutations to be sent in a single request to a GraphQL server.
Make sure to check out the library on Github for more usage examples.
Bonus
I've also created another validation library: No batched queries, which limits the number of all queries and mutations that could be sent per request. It pairs nicely with this validation, so you could allow, for example, 3 queries to be sent and then use noAlias
to disable duplicate queries.
Top comments (0)