When GraphQL field suggestions 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 field suggestion errors and information disclosure
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:
- the function doesn't exist (mmmh no... try again).
- Other functions exist:
searchHotels
,searchLeisures
andsearchUsers
.
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.
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
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.
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. How can you ensure that your development team handles errors properly over time and while your codebase evolves?
That’s why at Escape, we created a scanner that specializes in GraphQL APIs. It can streamline this process by quickly identifying issues across your API endpoints and providing remediation snippets—all in as little as 15 minutes, without complex integrations or traffic monitoring.