Back to Blog

Securing GraphQL Queries

GraphQL? Security? Queries? DoS? Server? EXPLOSION!!!
GraphQL is fast becoming one of the hottest API architecture paradigms out there with its predictable mechanism of exact data fetching, lexically strict yet dynamic type system, and efficient querying ability from multiple resources. It’s not a surprise that there has been mass adoption from both companies and developers, but what most aren’t considering is the security implications that come with on-boarding a new and still growing technology.
GraphQL makes the querying of data more efficient by fetching only the exact information that’s requested (or needed) - nothing more, nothing less. This is possible through its powerful query language, but there is some sacrifice that comes with this power. The queries themselves need to be in a certain structure making it predictable, which becomes a potential security risk. The queries are a new avenue of security concern that needs their own set of guidelines and strategies to mitigate certain risks.
A simple but potentially huge cyber threat is a Denial of Service (DoS) attack, which shuts down a machine or network by trying to continually exhaust all available resources that could slow down Central Processing Units (CPUs) and potentially crash servers. Assailants can send devious and complex queries, so the GraphQL server must be able to identify and properly handle these queries or risk having the server crash or get shut down. There are best-practices that can be implemented in a GraphQL API server to reduce the probability of DoS attacks successfully overwhelming and essentially taking control of your server. The following are potential strategies that can be implemented to increase your server’s defenses.

Query Timeout

The first strategy is to limit the maximum time a query will be processed in the server. If the timeout is set to 4 seconds, the server will only work on actions related to the query within that timeframe and will instantly stop executing once they have finished.
A few advantages of using timeout functionality is it’s relatively simple and easy to understand and implement. It can be used along with other strategies, specifically as a last line of defense.
The main disadvantage of this strategy is even after stopping query execution, the damage could have already been done. The assailant might have already gotten access within the timeout limit, so that’s why this is done as a last resort.

Query Size

A simple and unsophisticated approach is to directly check the size of the query and limit the queries being processed by your server based on a certain threshold. When receiving the GraphQL query from the client, it would be in the string data type, so creating a size limit based on the length of the string would be a good and easy way to do it.
app.use("*", (req, res, next) => {
const query = req.body.query;

if (query.length > 2500)
throw new Error("Query Size Limit");

return next();
});

Limiting the size is extremely easy to implement as shown in the image above.
The inherent flaw in limiting the size directly is it does not discriminate between good queries from bad ones. This means that a long valid query will not be allowed while a short devious query will still run in the server.

Query Amount

By using the node module graphql-input-number, this can be mitigated by limiting the maximum number of instances that can be retrieved by any given query.
import { GraphQLInputInt } from "graphql-input-number";

const amountLimit = GraphQLInputInt({
name: "amountLimit",
min: 1,
max: 150
});

type Company {
employees(first: amountLimit, after: String): [Person]
}

Using the “first” argument in a query, we can create a relatively short, but malicious query like the one below.
query {
miniServerDestroyer(name: "To The Enthusiastic") {
employees(first: 1000) { ... }
}
}

The query is fetching 1000 instances of the “employees” object. This means that this approach can be applied to any query retrieving however many instances of any object from a schema therefore potentially straining the server, network, and even the database.
Limiting query amount is relatively easy assigning the first argument the created custom scalar using graphql-input-number.
However, this approach will only work on the specific case of limiting the number of instances.

Query Depth

query {
serverDestroyer(name:"To The Unprepared") {
this{
that{
this{
that{
this{
that{
// 1 million times deeper
}
}
}
}
}
}
}
}

We can deduce from the query above that the types this and that can be infinitely nested on each other, making it exponentially more expensive. The previous strategies won’t necessarily account for this situation since limiting the size could potentially stop a query from executing, but it can easily be reduced to a point where it will go below the size limit, or when a timeout limit is set, the query would have already run and spent resources in its execution cycle.
Depth can be derived from a GraphQL query’s abstract syntax tree (AST) since it holds all the information (including the metadata) when it’s processed . As well as from the depth, a validationRule can be set to limit the maximum depth a query can have.
import depthLimitation from "graphql-depth-limit";

const depthLimit = depthLimitation(
10,
{ ignore: [/_trusted$/, "idontcare"] },
depths => console.log(depths)
);

Depth can be limited by using ‘graphql-depth-limit’ node module alongside validationRules. The first argument of depthLimit is the maximum depth of a query, while the second argument is an options object to specify fields to be ignored, and the third and last argument is a callback that would receive a map of depths object for each query executed.
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: resolvers,
validationRules: [depthLimit]
})
);

By limiting query depth to 10, the “serverDestroyer” query will be prevented from executing.
{
"errors": [
{
"message": "'serverDestroyer' exceeds max depth of 10",
"locations": [
{
...
}
]
}
]
}

A huge benefit of this approach is the query doesn’t execute because it simply analyzes the query’s AST, therefore reducing load on the server.
The same reason why limiting query amount is not a viable standalone strategy, limiting query depth also only covers a niche problem.

Query Complexity

query {
UltimateServerDestroyer(name: "A safe place for any weary developer") {
DESTRUUCTIIOOONN(first: 150) { ... }
EXPLOOOSIIIOOONSSS(first: 150) {
BAAMM { ... }
POOWW(first: 150) { ... }
WAAMM { ... }
KABBOOOOOOOM(first: 150) { ... }
}
}
}

By customizing the specific fields of a query like the one above, it can be just the right size, amount, and depth, but is potentially more dangerous and more resource-intensive than simply going all-in in a single approach. The result is the “Ultimate Server Destroyer” query and it will pass through all of the limitations provided by the previous strategies. To defend against this, we need to implement a new strategy that takes into account the overall theoretical complexity of a query and prevent ones that go above the set threshold.
There are npm modules that calculates and implements query cost analysis and some good packages are: graphql-validation-complexity, graphql-cost-analysis, and graphql-query-complexity. graphql-query-complexity and graphql-cost-analysis have the same idea and features except that the latter has directive and multiplier support, and has the most fine-grain control amongst the three npm modules. On the other hand, graphql-validation-complexity has a robust set of features and is the easiest to onboard.
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const complexityLimit = createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10, // Default is 0.
listFactor: 20, // Default is 10.
});
// Then use this rule with validate() or other validation APIs.

app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: resolvers,
validationRules: [complexityLimit]
})
);

This will limit the maximum complexity of a query to 1000 and custom configurations such as scalarCost, objectCost, and listFactor for scalars, objects, and lists.
This approach covers more situations than the previous strategies and it also analyzes the cost of a query even before executing the query, making it more efficient in terms of overall resources exhausted.
The problem is Query Complexity is hard to implement, more so when taking into account mutations because it becomes harder to define the estimated query cost for these queries.

Conclusion

GraphQL is a powerful and still growing technology, and it’s best to take security into consideration when onboarding a new technology. The strategies above are all viable options to include and implement into any GraphQL server, and while it’s in the discretion of the developers which strategies are best to integrate into their server, it’s undeniable that the security of GraphQL queries will soon become a point of focus as the popularity of GraphQL continues to skyrocket.