Error messages in GraphQL are seldom seen as a security concern, yet they should.
Default error messages are way too verbose. They can disclose critical information about the internal structure of your backend, or worse, private data like internal messages and private keys.
It lets malicious users easily run a fuzzy search on your API to find vulnerabilities. For example, if the error message contains information about the database models and how the user input is used to query data, an attacker could run requests until they figure out the right format and then exploit a vulnerability such as a SQL injection.
We found so many vulnerabilities in GraphQL apps coming from overly verbose error messages that we integrated a sensitive stack trace detection system in our GraphQL Security Testing Platform to alert developers that their APIs are vulnerable as early as in the CI/CD.
In this post, we’ll explain how GraphQL errors work, how to mask verbose errors but also how to create your custom errors leveraging the extendable GraphQL spec.
How errors work in GraphQL
GraphQL errors are fundamentally different from REST errors. You no longer rely on status codes and status texts.
According to the latest spec, the response of a GraphQL endpoint should always contain either the `data` field or the
errors field, and in some cases, both:
As you can see above, an error object has the following fields:
- message: the error message - if you throw an error in the resolver, its message will appear here;
- locations: contains the line and column (starting from 1) of the syntax element that is concerned with the error (in the example above, this is the "a" of "author". The column is 5 for the indent but would be 1 if we remove the indent in the query);
- path: the path of the response field which experienced the error (firstPost → author).
Additionally, you can add the
extensions field which is a map containing any custom field you see fit:
Note: it is recommended to use the extensions field for custom fields even though no error will be thrown when doing otherwise.
The code field of the extensions is usually implemented by the framework. For example, here are the default codes in Apollo:
- GRAPHQL_PARSE_FAILED (SyntaxError)
- GRAPHQL_VALIDATION_FAILED (ValidationError)
- BAD_USER_INPUT (UserInputError)
- UNAUTHENTICATED (AuthenticationError)
- FORBIDDEN (ForbiddenError)
- PERSISTED_QUERY_NOT_FOUND (PersistedQueryNotFoundError)
- PERSISTED_QUERY_NOT_SUPPORTED (PersistedQueryNotSupportedError)
- INTERNAL_SERVER_ERROR (None)
We’ll see below how you can define your own custom error at the end.
How to mask sensitive information and stack traces
Back to the initial problem: not giving up sensitive information to consumers of your API.
The errors in GraphQL are very comprehensive: they tell you what broke, where it broke both in the query and in your code. That is great for developer experience, but clearly not intended for the end-user.
Information like the stacktrace - the path tracing all the way back to the breaking point in your code - are usually found in the logs, not in the error message.
The solution to catch these default errors before they get sent to the end-user is to use a custom
formatError function in your GraphQL server (framework agnostic), to:
- log the error internally for debugging,
- update the error that you don’t want to expose to clients in production,
- send a *user-friendly* error.
Cool, now our API is not going to betray us.
But we can do more. Let’s see how we can leverage the GraphQL spec we saw above to create our own custom errors.
How to write custom errors
There are three key parts we can play within our GraphQL errors:
- the message: giving more context to the user, e.g., “password should have at least 6 characters”;
- the extensions code: this is a custom field but a good practice in API error design as it allows you to easily classify the different types of errors (Input error, Authorization error, Database error, etc)
- any other extensions field: this is where you get creative! you can add extra context, e.g., the field name that caused an error if the input is coming from a form.
Let’s go through an example.
The first step is to extend the GraphQLError class to create a custom error:
⚠️ As mentioned above, you should put custom fields under extensions to comply with the latest spec
Now you can throw the error where necessary:
Finally, make sure to return the
extensions field if you use a custom formatError function:
Alternatively, most GraphQL frameworks have a GraphQLError wrapper that lets you throw custom errors on the fly without writing new ones.
Error messages are just one of many GraphQL vulnerabilities. If you need an in-depth scan running dozens of security tests on your GraphQL endpoints, you should try out Escape!