Using Rate Limiting to protect your GraphQL API against Brute Force Attacks

For several days now, your users have been complaining about losing access to your web service. If at first you thought it was a simple coincidence and certainly a fault of the users, the incident starts to be strongly repeated and noticed.

You rush to your monitors and notice an abnormal rate of requests for a number of days. If you had any doubts before, it is now clear: you are undergoing a brute force attack. Fortunately, this article is here to help you avoid this.

What is a Bruteforce attack?

A brute force attack is a hacking method that uses trial and error to crack passwords, login credentials and encryption keys. It is a simple but reliable tactic for gaining unauthorized access to individual accounts and organizational systems and networks. The hacker tries several combinations, making requests to a server (in this case, your API) until he finds the correct login information.

If you have an unsecured login or registration form on your website, you are already easy prey. No login form should ever be released into the wild without at least some protection in place. We'll focus on rate limiting as a solution to brute force attacks.

What is rate limiting?

Rate limiting is a technique used to control the flow of data to and from a server by limiting the number of interactions a user can have with your API. This prevents a single user from using too many resources (either accidentally or on purpose, such as a malicious user).

Rate limiting illustrated

However, not all endpoints in your application need to be protected against brute force. For example, a private access point protected by a JWT token for internal use is not the top priority.


Just like performance and reliability, security is a requirement to ship production-ready applications. But GraphQL just lacked the proper tooling so many teams just skipped security...

That's why we built Escape GraphQL Security Platform! Start monitoring the security of your endpoints for free!


Remediation

In order to control this data flow in this way, we must first define criteria to prevent excessive requests from reaching our business logic code. How do we identify who should be blocked? The identifier is usually chosen as the most accurate information we have about our users. For example, if a user needs to register/login to our service, the identifier would certainly be their user ID. However, this is not always the case. Here are the most commonly used identifiers:

  1. A user id: a user id would be most effective for a service that authenticates users and assigns them unique identifiers.
  2. IP address: this method works best if you don’t have any other way to uniquely identify a user. While it works very well in some cases, you should be careful not to inadvertently block users with shared IP addresses.
  3. Geolocation: most malware attacks are known to originate from a handful of countries. With rate limiting, these countries can be isolated and blocked without affecting users from other regions. However, with this one, you may also temporarily block some innocent users...
Different identifiers

There are several algorithms that can be used to limit the rate of user interaction, each of which has its own drawbacks and advantages. In this article, we won't explain these algorithms in detail and will instead use a ready-to-use rate-limiting package for GraphQL: the npm graphql-rate-limit module. You will also need ioredis.

npm i graphql-rate-limit ioredis -s

Assuming we have the following GraphQL setup :

import { GraphQLServer } from 'graphql-yoga'

const typeDefs = `
  type Query {
    hello(name: String!): String!
  }
`

const resolvers = {
  Query: {
    hello: (_, { name }) => `Hello ${name}`
  }
}

const server = new GraphQLServer({ typeDefs, resolvers })

server.start(() => console.log('Server is running.'))

We can rate limit our resolver with the following code :

import { GraphQLServer } from 'graphql-yoga'

import * as Redis from "ioredis"
import { createRateLimitDirective, RedisStore } from "graphql-rate-limit"

export const redisOptions = {
  host: process.env.REDIS_HOST || "127.0.0.1",
  port: parseInt(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD || "somerandompassword",
  retryStrategy: times => {
    // reconnect after
    return Math.min(times * 50, 2000)
  }
}

const redisClient = new Redis(redisOptions)

const rateLimitOptions = {
  identifyContext: (ctx) => ctx?.request?.ipAddress || ctx?.id,
  formatError: ({ fieldName }) => 
    `You are doing way too much ${fieldName}`,
  store: new RedisStore(redisClient)
}

const rateLimitDirective = createRateLimitDirective(rateLimitOptions)

const resolvers = {
  Query: {
    hello: (_, { name }) => `Hello ${name}`
  }
}

// Schema
const typeDefs = `
  directive @rateLimit(
    max: Int
    window: String
    message: String
    identityArgs: [String]
    arrayLengthField: String
  ) on FIELD_DEFINITION

  type Query {
    hello(name: String!): String! @rateLimit(window: "1s", max: 2)
  }
`

const server = new GraphQLServer({ 
  typeDefs, 
  resolvers,
  schemaDirectives: {
    rateLimit: rateLimitDirective
  }
})

server.start(() => console.log('Server is running.'))

With your browser or a Postman client you can try to spam of request on http://localhost:4000 after some success you should see the error message that tells that you have reached the rate limit: You are doing way too much hello.

Here are the key parts of the presented implementation :

  • identifyContext This is where you decide whether you want to limit throughput by IP address, by user ID or by any other method.
  • store Connects to the Redis instance to store the necessary rate limiting data in your Redis server. Without a Redis store, this rate-limiting configuration cannot work. (Other stores can be used such as MongoDB).
  • createRateLimitDirective is a function that, if provided with rateLimitOptions, allows you to create dynamic rate-limiting directives that you can then connect to your GraphQL server for use in your scheme.

Drawbacks

Rate limiting has its drawbacks, however.

For example, if you use a geographic identifier, a sudden burst of legitimate traffic in a certain region means that new requests will be denied as the queue is filled. This will have the unintended consequence of making the user experience rather slow.

Conclusion

Despite its drawbacks, rate-limiting is a must-have feature to prevent your GraphQL server from being overwhelmed by API requests. It also protects your server from malicious "bruteforce" attacks, especially when deploying a public web service.

Apart from rate-limiting, it's important to follow other best practices when building GraphQL APIs. However, with the rapid pace at which APIs are being developed, it can be challenging to adhere to all best practices and keep track of what is being built. That's why we've developed Escape, a fully automated scanner that can identify all your exposed API endpoints, list all the issues, and help you fix them by providing code remediation snippets. The best part? We do all this without any complex integration or traffic monitoring. So what are you waiting for, go secure your APIs now.

Want to learn more about GraphQL security? Check out the following articles