When GraphQL Errors become a Security Issue

GraphQL offers an amazing developer experience. But the same features that help you in development, can disclose critical information to cyberattacks in production. Learn how you can fix this vulnerability.

When GraphQL Errors become a Security Issue

That's it. Your application is finally launched! 🎉

The development phase is over, and your platform is in production! New dynamic interface, GraphQL components, APIs, you name it. The world of travel booking is your oyster!

A few days later, it's the hard return to reality. Several unusual behaviours have been detected:

  • fake payments,
  • extraction of large numbers of reservations,
  • unexpected promotional discounts...

Nothing is going as expected. It's as if someone took control of your backoffice. And yet, no suspicious connections have occurred on the admin panel.

Digging a little deeper, you find out what happened: malicious people targeted your GraphQL endpoint directly, bypassing your admin tool.

How could they have known about the queries you had set up? You have only communicated the GraphQL documentation to a limited number of your contacts... In fact, you have left enabled functionalities that (almost) amount to giving access to your documentation to all users.

GraphQL Errors and Information Disclosure

Predictive Models
How suggestions may leak data – From xkcd

GraphQL offers a great developer experience with features that help you build your API in the development environment: readable errors and suggestions (stack trace), introspection, playground, etc.

However, not all these suggestions are designed to be kept beyond the development stage and into production. This is especially true for an application intended for public use. If GraphQL aims at helping developers, it does so with users as well - benevolent or not. And that is the problem: it can reveal information about your model, schema or your supported operations...

Let's take the example of our travel booking site.

Here, a single GraphQL API groups together both the user services (travel search, reservation, payment) and admin services (discounts, data consultation).

If the sent request is correct:

query SearchTrips {
    searchHotels(startDate: "2022-04-02", endDate: “2022-04-09”, typeOfAccomodation: “Hotel”){
        name
        price
        description
	}
}

GraphQL will return the expected results:

{
	"data": {
    	"searchHotels": [
        	{
				"name":  “Eldorado *****”,
				"price”: “3200 USD”,
				“description”: “The perfect place to relax.”
			},
            ...
        ]
    }
}

So far so good.

And if this time, one tries to search for activities with the following query:

query TrySearchActivities {
    searchActivities(startDate: "2022-04-02", endDate: “2022-04-09”){
        name
        price
        description
    }
}

QraphQL will return the following error in case it doesn’t exist:

{
	"errors": [
    	{
            "message": "Cannot query field \"searchActivities\" on type \"Query\". Did you mean \"searchHotels\",\”searchLeisures\” or \”searchUsers\”?",
            "locations": [{
            		"line": 144,
            		"column": 3
			}]
		}
    ]
}

This error message tells us two things:

  1. the function doesn't exist (mmmh no... try again).
  2. Other functions exist: searchHotels , searchLeisures and searchUsers.

Similarly, trying to call validateOrder may reveal the existence of validatePayment or validateDiscount functions.

This would probably have little impact if these functions were restricted to a small number of users. But it turns out, they were open to anyone, without any authentication mechanism. Not only does a flaw in your model exist, but it is also highlighted by GraphQL itself to "help" users.

Generally speaking, communicating this type of information to your users is equivalent to telling attackers which endpoints to target.

Many tools exist to make life easier for Pentesting (and thus for attacks!), such as Clairvoyance or GraphQLMap. It is therefore in your interest to avoid this type of attack.

Graphinder: lightweight and blazing fast GraphQL endpoint finder
Introducing Graphinder, a lightweight and blazing fast GraphQL endpoint finder, making penetration testing on GraphQL much faster ⚡️

Right, but how do we fix the situation?


If you want to catch DoS vulnerabilities and 100+ other GraphQL security vulnerabilities before it's too late, checkout Escape. Run hundreds of security scans in your CI/CD 🚀


Handling GraphQL Errors to avoid Information Disclosure

Differentiate between your production and development environments

As previously mentioned, the help that GraphQL provides you with during development is not always good to keep once the application is in production.

A good practice is to have at least two types of environments: development and production.

In the development environment, you can enable introspection, automatic suggestions for fields and functions, stack trace, and tools such as GraphiQL.

In production though, they should be disabled, while reinforcing the security level (switching to HTTPS for instance).

You can use the application's configuration arguments and simply modify them between these two environments. Most of the time it is enough to change the value of the parameters used by the application.

Your code could be something similar to the following (example in NodeJS):

async function startGraphQLServer(){
	const configurations = {
    	production: { ssl:true, port:443, hostname:"example.com", introspection:false },
        development: { ssl:false, port:4000, hostname:"localhost", introspection:true }
    };
    // Read the environment from the variables
    const environment = process.env.NODE_ENV || "production";
    // Set the parameters depending on the chosen environment
    const config = configurations[environment];
    // Then you can use config introspection a parameter for your server
	...
}

Disable field suggestions

The solution that concerns us is to prevent GraphQL from making suggestions to users in the case of a wrong operation name.

Instead of returning something like "Did you mean that query", it is preferable to return something along the lines of "That query doesn't exist" without providing alternatives.

Either the person knows the correct query and made a mistake, or they are not supposed to have access to it and therefore do not need to be told.

Different mechanisms exist and vary according to the implementation of GraphQL you picked.

Apollo GraphQL

By default, Apollo relies on the NODE_ENV environment variable to disable introspection. This is a feature that allows the entire schema and queries to be discovered. Unfortunately, it is not possible (at this time) to disable suggestions directly in Apollo. There are workarounds, such as (this also works for Node):

import {ValidationError} from "apollo-server-express";
import {GraphQLError, GraphQLFormattedError} from "graphql";

export const formatGQLError = (error: GraphQLError):GraphQLFormattedError () => {
	if (process.env.NODE_ENV === "production") {
		if (error instanceof ValidationError) {
			return new ValidationError("Invalid request.")
		}
	}
    return error
}

From this Github issue

This one is a bit radical since it replaces all ValidationError with an "Invalid request" message, but it has the advantage of avoiding returning to users the suggestions made by the GraphQL engine.

Hasura

Hasura is not concerned by suggestions even in its default configuration. It is of course necessary to disable introspection to avoid that the schema is directly communicated to the user. This can be done via the administration console.

Graphene

For Graphene, and more generally for applications using the python kernel to run GraphQL, there is again no direct parameter, but it is possible to prevent these suggestions (see that Github issue) by configuring a parameter:

graphql.pyutils.did_you_mean.MAX_LENGTH = 0

If you are using Graphene with Django, this setting can be done in your __init__.py file with the following instruction:

from graphql.pyutils import did_you_mean

did_you_mean.__globals__["MAX_LENGTH"] = 0

Of course, you must also disable introspection, otherwise, it is useless. You can find here a solution that works for Graphene with Django.

Setting up authentication and authorization mechanisms

Code Talkers
Security through obscurity (from xkcd)

Of course, assuming that a hidden query will never be discovered by malicious people is a mistake. “Security through obscurity" (see reference) can be useful but should never be the only means of protection.

In your case, you should make sure that authentication and permissions for your sensitive queries are properly configured and that no one can use them without being explicitly allowed to do so.

Authentication, Authorization and Access Control for GraphQL
Confusion between authentication and authorization causes data leaks. Learn the difference and how to implement the right access control pattern in your GraphQL API.

Conclusion

GraphQL provides developers with a number of tools to help them discover and understand the underlying schema.

However, this information can also be useful to an attacker once the system has gone into production. You should therefore ensure that the bare minimum of information is communicated to your users. This can be done by disabling introspection and suggestions. However, "obscurity" is not a viable security strategy. In addition, you must set up an adequate authentication and authorisation system.

Reference