How to make your GraphQL implementation more secure
June 8, 2022
Table of contents
"Many developers love GraphQL because it uses a single endpoint and enables clients to customize their requests to obtain the data sets they want. However, when it comes to security, GraphQL’s flexibility can introduce specific security vulnerabilities that every developer needs to consider.
When some of our developers began using GraphQL at Nord Security, it was my job as part of the core web security team to analyze the language’s potential vulnerabilities. Hopefully, the problems and the fixes that we found will help make your implementation more secure as well.
Aim for secure configurations
Many GraphQL implementations have potentially insecure settings enabled by default that expand the application’s attack surface.
To introspect or not to introspect
Introspection is a powerful feature in GraphQL that is used to retrieve information about the queries, types, fields, and mutations it supports. It is extremely useful for clients that consume GraphQL endpoints as it provides comprehensive documentation and makes integration a lot easier. However, this comes with a cost. This functionality can be abused to retrieve GraphQL schema, which could help attackers search systems for vulnerabilities.
Having GraphQL introspection in production is usually not considered a vulnerability by itself. Sometimes it makes sense to have it if the API is public and is meant to be used by developers outside of your organization. For private APIs, however, it might expose sensitive information and expand the attack surface. A rule of thumb is to disable introspection in production unless you have a good reason not to. Even in this case, consider allowing introspection only for authorized requests.
In addition to introspection, developers can use GraphiQL (GraphQL IDE), which offers a user-friendly interface to explore the GraphQL schema. While it’s useful in development, it will pose the same risk as introspection if left in production, so make sure it’s disabled.
Make fuzzing fields and queries more difficult
Even when introspection is disabled, attackers will try to fuzz field and query values to find sensitive operations that can be exploited further. GraphQL makes this task easier with its built-in Field Suggestions feature. When a typo is made in the field name, GraphQL will try to suggest an existing field that is similar to the one requested.
1{2"errors":[3{4"message":5"Cannot query field \"prices\" on type \"Plan\". Did you mean \"price\"?",6"locations":[7{8"line":1,9"column":5210}11],12"extensions":{13"code":GRAPHHQL_VALIDATION_FAILED"14}15}16],17"data":null18}
This is a typical error returned from GraphQL endpoint when trying to request a field (in this case prices) which doesn't exist in schema. GraphQL Field Suggestion feature reveals that the actual field name is price.
This can be leveraged to enumerate the GraphQL schema and reveal sensitive field names. Therefore, you should consider disabling this feature for private APIs if your GraphQL implementation supports this.
Watch out for excessive errors
GraphQL’s detailed errors help developers accurately identify issues in development. However, default error messages are too verbose and can pose a security risk by disclosing sensitive information about your backend infrastructure and exposing vulnerable implementations. Therefore you should never run GraphQL in debug mode in production. Instead, have a middleware to consume those verbose errors and stack traces, log them securely, and return user-friendly errors to end-users.
Protect from Denial of Service attacks
One of the most common ways to mitigate application-layer DoS attacks is by limiting requests to API endpoints from specific IP addresses. This prevents the API from being flooded with requests, thus exhausting system resources and becoming inaccessible for normal users. While rate limiting requests to the GraphQL endpoint are also important and should be implemented, it will rarely be enough to prevent DoS attacks. With GraphQL, a single well-crafted query can bring down the whole server. Therefore, you need additional security measures to ensure your system remains available.
Implement max depth checks
The flexibility of GraphQL allows clients to craft complex and nested queries, thus obtaining the exact data set they need. The problem, however, is that many GraphQL schemas have cyclic graphs that can be abused in malicious queries. Let’s look at a deeply nested example query:
1query circularQuery {2product {3plans {4product {5plans {6product {7# and so on...8}9}10}11}12}13}
Each new nesting level increases the response size exponentially, so a sufficiently deep query may exhaust your server resources and would render it unavailable. You can prevent this by limiting GraphQL query depth. This will require a custom implementation as it’s not usually natively supported by GraphQL. Set the depth to a reasonable number that won’t cause difficulties for your API consumers:
1{2"errors":[3{4"message": operation has depth 19, which exceeds the limit of 6",5"extensions":{6"code": "DEPTH_LIMIT_EXCEEDED"7}8}9],10"data":null11}
Example of the nesting protection implemented in GraphQL API. The error shows that the query was rejected because its depth limit was exceeded.
Consider query complexity
Having nesting protection may not be enough to prevent complex queries from causing security issues. Certain fields can return more nodes than others, making them computationally expensive. This is where query cost analysis comes into play. Assign cost values to different fields and reject the query if the complexity exceeds the limit:
1{2"errors":[3{4"message": operation has complexity 312, which exceeds the limit of 160",5"extensions":{6"code": "DEPTH_LIMIT_EXCEEDED"7}8}9],10"data":null11}
Error returned from GraphQL API that has query complexity checks implemented, indicating that the query sent in the request exceeded the allowed complexity and therefore was rejected.
Query complexity checks usually take more effort to implement and may not be needed in all cases. Before choosing this route, evaluate the complexity of your GraphQL schema and decide whether this prevention is really necessary.
Look out for batching attacks
GraphQL lets users send multiple queries in a single request. This is called query batching, and despite its many benefits, it may also allow for harder-to-detect exploits. This may result not only in DoS attacks but also in object enumeration, brute force attacks, and rate limit bypass. WAFs (Web Application Firewalls) or IDS (Intrusion Detection Systems) might not detect these attacks as they will only result in a single request like this:
1mutation {2a1: login(username: "testuser", password: "graphql1") {3token4}5a2: login(username: "testuser", password: "graphql2") {6token7}8a3: login(username: "testuser", password: "graphql3") {9token10}11#....12}
This is an example of the query batching that can be leveraged to password bruteforce attack.
Some mitigation techniques may include limiting the number of operations that can be batched and preventing batching for sensitive objects.
Cache your data
While the above techniques will solve some of GrapQL’s specific security issues, there may be situations when this will not be enough. This might be either a bug in implementation or a normal API usage scenario that you didn’t think of. Consider the below example:
1query plans (product: [NordVPN, NordVPN, NordVPN, NordVPN....]) {2id3price4currency5}
The same field value is passed numerous times, resulting in large amounts of DB queries. To prevent these kinds of problems from happening, consider implementing caching for your queries. This will prevent DoS attacks and increase the efficiency of your GraphQL API by limiting resource consumption.
Mind the other API vulnerabilities
While GraphQL has its own specific security issues that need to be addressed, it’s no different from other technologies in terms of API security. It is also prone to injections, IDORs, authentication and authorization failures, and other common security vulnerabilities. It’s important to follow secure development practices and do proper security testing before deploying your API to production.
Don’t forget to monitor
It’s always important to monitor your systems and the GraphQL endpoint is no exception. Even the most thorough implementations can cause issues, so keep an eye on:
Query performance. Are there any signs of slow queries that need to be optimized?
Significant response delays. How long is it taking for the endpoint to respond?
Errors and failures. Are there any validation or query parsing errors or failures in the database?
Monitoring your GraphQL endpoint will not only help to spot security issues, but also identify any potential bottlenecks in your data fetching pipeline so that you can optimize your endpoint for better performance.