Advanced GraphQL: Securing GraphQL API from Malicious Queries #13

Security risks associated with GraphQL APIs a few strategies to mitigate these risks.



GraphQL gives enormous power to clients. But with great power come great responsibilities 🕷.

Since clients have the possibility to craft very complex queries, our servers must be ready to handle them properly. These queries may be abusive queries from evil clients, or may simply be very large queries used by legitimate clients. In both of these cases, the client can potentially take your GraphQL server down.

There are a few strategies to mitigate these risks. We will cover them in this chapter in order from the simplest to the most complex
. . .

Maximum Query Depth

As we covered earlier, clients using GraphQL may craft any complex query they want. Since GraphQL schemas are often cyclic graphs, this means a client could craft a query like this one:
query IAmEvil { author(id: "abc") { posts { author { posts { author { posts { author { # that could go on as deep as the client wants! } } } } } } } }

What if we could prevent clients from abusing query depth like this? Knowing your schema might give you an idea of how deep a legitimate query can go. This is actually possible to implement and is often called Maximum Query Depth.

By analyzing the query document’s abstract syntax tree (AST), a GraphQL server is able to reject or accept a request based on its depth.

Take for example a server configured with a Maximum Query Depth of 3, and the following query document. Everything within the red marker is considered too deep and the query is invalid

Image Credits: https://www.howtographql.com/
Image Credits: https://www.howtographql.com/
We looked around and found graphql-depth-limit, a lovely module by Andrew Carlson, which enables us to easily limit the maximum depth of incoming queries
app.use('/api', graphqlServer({
validationRules: [depthLimit(10)]
}));

That’s how simple depth limiting is!
. . .

Query Complexity

Sometimes, the depth of a query is not enough to truly know how large or expensive a GraphQL query will be. In a lot of cases, certain fields in our schema are known to be more complex to compute than others.

Query complexity allows you to define how complex these fields are, and to restrict queries with maximum complexity. The idea is to define how complex each field is by using a simple number. A common default is to give each field a complexity of 1. Take this query for example:
query {
author(id: "abc") { # complexity: 1
posts { # complexity: 1
title # complexity: 1
}
}
}

A simple addition gives us a total of 3 for the complexity of this query. If we were to set a max complexity of 2 on our schema, this query would fail.

What if the posts field is actually much more complex than the author field? We can set a different complexity to the field. We can even set a different complexity depending on arguments! Let’s take a look at a similar query, where posts has a variable complexity depending on its arguments:
query { author(id: "abc") { # complexity: 1 posts(first: 5) { # complexity: 5 title # complexity: 1 } } }

There are a couple of packages on npm to implement query cost analysis. Our two front-runners were graphql-validation-complexity, a plug-n-play module, or graphql-cost-analysis, which gives you more control by letting you specify a @cost directive.
. . .

Query Whitelisting

A second approach we considered was to have a whitelist of approved queries we use in our own application and telling the server to not let any query pass except for those.
app.use('/api', graphqlServer((req, res) => {
const query = req.query.query || req.body.query;
// TODO: Get whitelist somehow
if (!whitelist[query]) {
throw new Error('Query is not in whitelist.');
}
/* ... */
}));

Maintaining that list of approved queries manually would obviously be a pain, but thankfully the Apollo team created persistgraphql, which automatically extracts all queries from your client-side code and generates a nice JSON file out of it.
This technique works beautifully and will block all vicious queries reliably. Unfortunately, it also has two major tradeoffs:
  1. We can never change or delete queries, only add new ones: if any user is running an outdated client we can’t just block their request. We’d likely have to keep a history of all queries ever used in production which is a lot more complex.
  2. We cannot open our API to the public: sometime in the future we’d like to open up our API to the public so that other developers can build their interpretation. If we only let a whitelist of queries through that severely limits their options already and defeats the point of having a GraphQL API (super flexible system restrained by a synthetic whitelist). Those were constraints we couldn’t work with, so we went back to the drawing board.
. . .

Throttling

The solutions we’ve seen so far are great to stop abusive queries from taking your servers down. The problem with using them alone like this is that they will stop large queries, but won’t stop clients that are making a lot of medium sized queries!
In most APIs, a simple throttle is used to stop clients from requesting resources too often.

GraphQL is a bit special because throttling on the number of requests does not really help us. Even a few queries might be too much if they are very large.

In fact, we have no idea what amount of requests is acceptable since they are defined by the clients. So what can we use to throttle clients?

A good estimate of how expensive a query is the server time it needs to complete. We can use this heuristic to throttle queries. With a good knowledge of your system, you can come up with a maximum server time a client can use over a certain time frame.

We also decide on how much server time is added to a client over time. This is a classic leaky bucket algorithm. Note that there are other throttling algorithms out there, but they are out of scope for this chapter. We will use a leaky bucket throttle in the next examples.
. . .

Timeout

The first strategy and the simplest one is using a timeout to defend against large queries. This strategy is the simplest since it does not require the server to know anything about the incoming queries. All the server knows is the maximum time allowed for a query.

For example, a server configured with a 5 seconds timeout would stop the execution of any query that is taking more than 5 seconds to execute.
. . .
In the next tutorial, we'll talk about how handle authentication and authorization when using GraphQL.


Eric Leslie

Jun 27 2019

Write your response...

On a mission to build Next-Gen Community Platform for Developers