DEV Community

Cover image for Preventing GraphQL batching attacks
Ivan V.
Ivan V.

Posted on

Preventing GraphQL batching attacks

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")
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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 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)
Enter fullscreen mode Exit fullscreen mode

Now if you send a query like this:

  query {
    hello
    hello_2: hello
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

And for the mutation:

  mutation {
    login(pass: 1111, username: "ivan")
    second_login: login(pass: 2222, username: "ivan")
  }
Enter fullscreen mode Exit fullscreen mode

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
  })
)
Enter fullscreen mode Exit fullscreen mode

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)