DEV Community

Cover image for How to Handle GraphQL Errors for a Secure API
Achraf
Achraf

Posted on • Updated on • Originally published at blog.escape.tech

How to Handle GraphQL Errors for a Secure API

Error messages in GraphQL are rarely considered as a security matter... yet they 100% should!

The default error messages are way too verbose. They could disclose information about your internal infrastructure such as what database you use, or worse, leak your secret keys.

Error messages give direct feedback for hackers to run a fuzzy search (finding patterns through trial and error) on your API until they find vulnerabilities.

For example, if the error message contains information about the database schema and the query used to retrieve user information, a hacker could figure out a way to perform a SQL injection and get the information of EVERY user in your database, bypassing any access control 🀯

Handling error messages properly is one of the most essential step of building a secure GraphQL API (see 9 GraphQL security best practices).

Let's see how GraphQL errors work, how to mask verbose messages but also how to create your own custom errors by leveraging the flexible GraphQL spec.

This was originally posted on blog.escape.tech

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:

Example of error message

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:

https://spec.graphql.org/October2021/#example-8b658<br>

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:

  1. log the error internally for debugging,
  2. update the error that you don’t want to expose to clients in production,
  3. send a user-friendly error.
// api.js

formatError: (err) => {
    // 1. Log the error for internal debugging
    logger(err, req, Date.now())

    // 2. Update the error message
    if (err.message.includes('Database Error')) {
        err.message = 'Internal server error'
    }

    // 3. Return the error message
    return {
        ...err
        stack: process.env.NODE_ENV === 'production' ? undefined : err.stack
    }
}
Enter fullscreen mode Exit fullscreen mode

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 error.

How to write custom errors

There are three key components we can play with to build custom errors:

  1. the message: giving more context to the user, eg. "password should have at least 6 characters",
  2. the extensions code: an optional field, but a good practice in API error design to easily classify the different types of errors in the frontend (eg. with ⚠️ WARNING, ❌ ERROR or ℹ️ INFO callouts)
  3. any other extensions field: this is where you can get creative and add any extra context, eg. the field name that cause an error in the case of a form submission.

Let's run through an example with real code!

The first step is to extend the GraphQLError class to create a custom error:

import {GraphQLError, GraphQLErrorExtensions} from 'graphql'

class InputValidationError extends GraphQLError {
    extensions: GraphQLErrorExtensions;
    constructor(message: string, field: string) {
        super(message);
        this.extensions = {
            statusCode: 500,
            code: "INPUT_VALIDATION_FAILED",
            field,
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ The latest spec highly recommend putting any custom fields under extensions.

Now you can throw the error where necessary:

post: (_parent:null, {id}: QueryPostArgs, context: Context) => {
    if(id >= context.db.posts.length) {
        throw new InputValidationError("Index out of range", "id")
    }
    // Fetch and return the corresponding post
}
Enter fullscreen mode Exit fullscreen mode

Finally, make sure to return the extensions field if you use a custom formatError function:

formatError: (err) => {
    return {
        message: err.message,
        extensions: err.extensions, // this is already returned by default
        stack: process.env.NODE_ENV === 'production' ? undefined : err.stack
    }
}
Enter fullscreen mode Exit fullscreen mode

And voila!

Custom error in GraphQL Playground

Alternatively, most GraphQL frameworks have a GraphQLError wrapper that lets you throw custom errors on the fly without writing new ones.

import {ApolloError} from 'apollo-server-errors

throw new ApolloError('Index out of range', 'INPUT_VALIDATION_FAILED', {field: 'id'})
Enter fullscreen mode Exit fullscreen mode

GraphQL Security

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!

Escape dashboard

Top comments (0)