Как обрабатывать ошибки GraphQL для обеспечения безопасности API

Сообщения об ошибках в GraphQL редко рассматриваются как вопрос безопасности… но они 100% должны рассматриваться!

Сообщения об ошибках по умолчанию слишком многословны. Они могут раскрыть информацию о вашей внутренней инфраструктуре, например, какую базу данных вы используете, или, что еще хуже, привести к утечке ваших секретных ключей.

Сообщения об ошибках дают хакерам прямую обратную связь для нечеткого поиска (поиск закономерностей методом проб и ошибок) в вашем API, пока они не найдут уязвимости.

Например, если сообщение об ошибке содержит информацию о схеме базы данных и запросе, используемом для получения информации о пользователе, хакер может найти способ выполнить SQL-инъекцию и получить информацию о КАЖДОМ пользователе в вашей базе данных в обход любого контроля доступа 🤯.

Правильная обработка сообщений об ошибках — один из самых важных этапов создания безопасного GraphQL API (см. 9 лучших практик безопасности GraphQL).

Давайте посмотрим, как работают ошибки GraphQL, как маскировать многословные сообщения, а также как создавать свои собственные ошибки, используя гибкую спецификацию GraphQL.

Эта статья была первоначально опубликована на blog.escape.tech

Как работают ошибки в GraphQL

Ошибки GraphQL принципиально отличаются от ошибок REST. Вы больше не полагаетесь на коды статуса и тексты статуса.

Согласно последней спецификации, ответ конечной точки GraphQL всегда должен содержать либо поле data, либо поле errors, а в некоторых случаях и то, и другое:

Как вы можете видеть выше, объект ошибки имеет следующие поля:

  • сообщение: сообщение об ошибке — если вы бросили ошибку в резольвер, ее сообщение появится здесь
  • locations: содержит строку и столбец (начиная с 1) синтаксического элемента, который связан с ошибкой (в примере выше это «a» автора. Столбец равен 5 из-за отступа, но будет равен 1, если мы уберем отступ в запросе)
  • path: путь к полю ответа, в котором произошла ошибка (firstPost → author).

Кроме того, вы можете добавить поле extensions, которое представляет собой карту, содержащую любое пользовательское поле по вашему усмотрению:

Примечание: рекомендуется использовать поле extensions для пользовательских полей, даже если в противном случае ошибка не возникнет.

Поле кода расширений обычно реализуется фреймворком. Например, вот коды по умолчанию в Apollo:

Ниже мы рассмотрим, как вы можете определить свою собственную пользовательскую ошибку в конце.

Как скрыть конфиденциальную информацию и трассировку стека

Вернемся к исходной проблеме: не выдавать конфиденциальную информацию потребителям вашего API.

Ошибки в GraphQL очень полные: они говорят вам, что сломалось, где сломалось, как в запросе, так и в вашем коде. Это отлично подходит для разработчиков, но явно не предназначено для конечного пользователя.

Такая информация, как stacktrace — трассировка пути до точки поломки в вашем коде — обычно содержится в логах, а не в сообщении об ошибке.

Решение для отлова этих ошибок по умолчанию до того, как они будут отправлены конечному пользователю, заключается в использовании пользовательской функции formatError в вашем GraphQL-сервере (не зависящей от фреймворка), чтобы:

  1. внутренне регистрировать ошибку для отладки,
  2. обновить ошибку, которую вы не хотите раскрывать клиентам в продакшене,
  3. отправлять удобную для пользователя ошибку.
// 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
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Круто, теперь наш API не предаст нас.

Но мы можем сделать больше.

Давайте посмотрим, как мы можем использовать спецификацию GraphQL, которую мы видели выше, для создания собственной пользовательской ошибки.

Как писать пользовательские ошибки

Есть три ключевых компонента, с которыми мы можем играть для создания пользовательских ошибок:

  1. сообщение: предоставление пользователю большего контекста, например, «пароль должен содержать не менее 6 символов»,
  2. код расширения: необязательное поле, но хорошая практика в дизайне ошибок API, чтобы легко классифицировать различные типы ошибок во фронтенде (например, с помощью ⚠️ WARNING, ❌ ERROR или ℹ️ INFO).
  3. любое другое поле расширения: здесь вы можете проявить творческий подход и добавить любой дополнительный контекст, например, имя поля, которое вызывает ошибку в случае отправки формы.

Давайте рассмотрим пример с реальным кодом!

Первым шагом будет расширение класса GraphQLError для создания пользовательской ошибки:

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,
        };
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

⚠️ Последняя спецификация настоятельно рекомендует помещать любые пользовательские поля в extensions.

Теперь вы можете бросить ошибку там, где это необходимо:

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
}
Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, не забудьте вернуть поле extensions, если вы используете пользовательскую функцию formatError:

formatError: (err) => {
    return {
        message: err.message,
        extensions: err.extensions, // this is already returned by default
        stack: process.env.NODE_ENV === 'production' ? undefined : err.stack
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

И вуаля!

В качестве альтернативы, большинство GraphQL-фреймворков имеют обертку GraphQLError, которая позволяет вам бросать пользовательские ошибки на лету без написания новых.

import {ApolloError} from 'apollo-server-errors

throw new ApolloError('Index out of range', 'INPUT_VALIDATION_FAILED', {field: 'id'})
Вход в полноэкранный режим Выход из полноэкранного режима

Безопасность GraphQL

Сообщения об ошибках — это лишь одна из многих уязвимостей GraphQL. Если вам нужно глубокое сканирование, выполняющее десятки тестов безопасности на конечных точках GraphQL, вам стоит попробовать Escape!

Оцените статью
Procodings.ru
Добавить комментарий