Статья по безопасности GraphQL (GraphQL Cheat Sheet)

Введение

GraphQL — открытый язык запросов (изначально Facebook) для построения API как альтернативы REST и SOAP. Популярен с 2012 года за гибкость для разработчиков и клиентов. Есть серверы и клиенты на разных языках. Многие компании используют GraphQL, в том числе GitHub, Credit Karma, Intuit и PayPal.

Эта статья охватывает ключевые темы безопасности GraphQL:

Типичные атаки

Практики и рекомендации

Валидация входных данных

Строгая валидация снижает риск инъекций и DoS. В GraphQL клиент передаёт идентификаторы, а бэкенд через резолверы ходит в HTTP, БД и др. Ввод попадает во внешние вызовы — это типичная поверхность для инъекций и DoS.

См. шпаргалки OWASP: Input Validation и Injection Prevention.

Общие практики

Принимайте только допустимые значения (белый список).

Защита от инъекций

Если ввод передаётся в другой «интерпретатор» (SQL/NoSQL/ORM, ОС, LDAP, XML):

  • Предпочитайте библиотеки с безопасным API (параметризация и т.п.).
    • Следуйте документации.
    • ORM/ODM полезны, но при неправильном использовании возможны ORM-инъекции.
  • Иначе экранируйте/кодируйте по правилам целевого языка; выбирайте поддерживаемые библиотеки.

Дополнительно:

Валидация процессов

Даже санитизированный ввод не должен управлять потоком данных без необходимости: например, не выполняйте HTTP-запросы на хост, который задал пользователь (если нет жёсткой бизнес-потребности).

Защита от DoS

DoS бьёт по доступности и стабильности API. Ниже — меры на уровне приложения и стека; отдельная шпаргалка — DoS.

Специфично для GraphQL:

  • ограничение глубины запросов;
  • ограничение «количества» (amount) объектов в запросе;
  • пагинация;
  • разумные таймауты на уровне приложения и/или инфраструктуры;
  • анализ стоимости запроса и верхний предел;
  • rate limiting по IP и/или пользователю;
  • батчинг и кэш на сервере (DataLoader).

Ограничение глубины и количества

У запроса есть глубина вложенности и число запрашиваемых элементов (например first: 99999999). По умолчанию ограничений может не быть — риск DoS. Задайте лимиты (часто небольшая своя реализация). См. Apollo и How to GraphQL. Пагинация также помогает производительности.

graphql-java: MaxQueryDepthInstrumentation. JavaScript: graphql-depth-limit, graphql-input-number.

Пример запроса большой глубины:

query evil {            # Depth: 0
  album(id: 42) {       # Depth: 1
    songs {             # Depth: 2
      album {           # Depth: 3
        ...             # Depth: ...
        album {id: N}   # Depth: N
      }
    }
  }
}

Пример запроса с огромным first:

query {
  author(id: "abc") {
    posts(first: 99999999) {
      title
    }
  }
}

Таймауты

Таймауты ограничивают ресурсы на один запрос, но срабатывают не всегда вовремя. Значение зависит от API и способа получения данных.

На уровне приложения — таймауты для запросов и резолверов (в GraphQL из коробки нет — нужен свой код). См. статью в Medium и примеры ниже.

Пример таймаута (JavaScript) — фрагмент из ответа на Stack Overflow:

request.incrementResolverCount = function () {
    var runTime = Date.now() - startTime;
    if (runTime > 10000) {  // таймаут 10 с
      if (request.logTimeoutError) {
        logger('ERROR', `Request ${request.uuid} query execution timeout`);
      }
      request.logTimeoutError = false;
      throw 'Query execution has timeout. Field resolution aborted';
    }
    this.resolverCount++;
  };

Пример таймаута (Java) через Instrumentation

public class TimeoutInstrumentation extends SimpleInstrumentation {
    @Override
    public DataFetcher<?> instrumentDataFetcher(
            DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters
    ) {
        return environment ->
            Observable.fromCallable(() -> dataFetcher.get(environment))
                .subscribeOn(Schedulers.computation())
                .timeout(10, TimeUnit.SECONDS)  // таймаут 10 с
                .blockingFirst();
    }
}

Инфраструктурный таймаут

Проще задать таймаут на HTTP-сервере (Apache, nginx), reverse proxy или балансировщике; такие таймауты грубее и их чаще обходят, чем логику в приложении.

Анализ стоимости запроса

Полям/типам назначают «стоимость»; слишком дорогие запросы отклоняются. Реализация сложная и не всегда нужна — сначала проверьте API «тяжёлым» запросом на стенде. Цитата Apollo:

Прежде чем тратить много времени на анализ стоимости, убедитесь, что он вам нужен. Попробуйте «положить» staging API тяжёлым запросом — возможно, у вас нет глубокой вложенности или тысячи записей за раз обрабатываются нормально.

Подробнее — в разделе «Query Cost Analysis» той же статьи Apollo.

graphql-java: встроенный MaxQueryComplexityInstrumentation. JavaScript: graphql-cost-analysis или graphql-validation-complexity.

Ограничение частоты запросов

Лимиты по IP или пользователю (в т.ч. для анонимов) снижают спам. Удобно делать на WAF, API-шлюзе или веб-сервере (Nginx, Apache / mod_evasive). Реализация в коде сложнее. Про throttling в GraphQL — How to GraphQL.

Батчинг и кэш на сервере

Чтобы не дублировать запросы за одними и теми же данными в коротком окне, используйте батчинг и кэширование; например DataLoader.

Ресурсы системы

Без лимитов CPU/памяти API уязвим к DoS. На Linux — cgroups, ulimits, LXC; в контейнерах проще — см. раздел про лимиты ресурсов в Docker Security Cheat Sheet.

Контроль доступа

  • Всегда проверяйте, что субъект имеет право читать или менять запрашиваемые данные (RBAC и др.). Это снижает риск IDOR, включая BOLA и BFLA.
  • Проверки авторизации на рёбрах и на узлах графа (см. пример: на рёбрах было, на узлах — нет).
  • Интерфейсы и union помогают возвращать разный набор полей в зависимости от прав.
  • В резолверах query/mutation можно встраивать проверки, в т.ч. через middleware RBAC.
  • Отключите интроспекцию в продакшене и на публичных контурах.
  • Отключите GraphiQL и аналоги в продакшене / на публичных URL.

Доступ к объектам по ID

Часто в запросе передаётся прямой ID сущности (как первичный ключ в БД). Наличие ID не означает права доступа; иначе это BOLA / IDOR.

В схеме могут быть поля node / nodes для прямого доступа по ID. Проверка: cat schema.json | jq ".data.__schema.types[] | select(.name==\"Query\") | .fields[] | .name" | grep node. Удаление полей из схемы отключает путь, но авторизацию всё равно нужно проверять явно.

Доступ к полям (чтение)

Решите, могут ли разные клиенты читать разные наборы полей; при необходимости проверяйте в коде право на каждое поле.

Доступ к мутациям (изменение)

Если разрешены мутации, ограничьте, кто и какие поля может менять (только чтение для части клиентов, разные роли и т.д.).

Атаки через batching

GraphQL позволяет батчить запросы — несколько операций или несколько объектов за один HTTP-вызов, что даёт batching-атаку: быстрее и менее заметно, чем классический брутфорс. Типичный формат:

[
  { query: QUERY_0, variables: VARS_0 },
  { query: QUERY_1, variables: VARS_1 },
  { query: QUERY_N, variables: VARS_N }
]

Пример одного запроса с несколькими экземплярами droid:

query {
  droid(id: "2000") {
    name
  }
  second:droid(id: "2001") {
    name
  }
  third:droid(id: "2002") {
    name
  }
}

Риски: DoS на уровне приложения, перечисление сущностей, брутфорс паролей/OTP/сессий; WAF/SIEM могут не увидеть множество операций в одном запросе; лимиты nginx по числу запросов не сработают.

Смягчение batching-атак

Лимиты на уровне кода «на один запрос»:

  • ограничить число запрашиваемых объектов;
  • запретить батчинг для чувствительных сущностей;
  • ограничить число операций в одном батче.

Можно считать число разных объектов в одном вызове и блокировать после порога; для чувствительных данных отключить батчинг, чтобы атакующий вынужден был слать отдельные запросы как к REST. Комбинируйте меры.

Безопасная конфигурация

Часто по умолчанию включено нежелательное поведение:

  • не возвращайте избыточные ошибки (без стека и debug в проде);
  • ограничьте или отключите интроспекцию и GraphiQL по политике;
  • учитывайте подсказки при опечатках полей, если интроспекция выключена.

Интроспекция и GraphiQL

Часто интроспекция и GraphiQL доступны без аутентификации — раскрывается схема, мутации, устаревшие и «внутренние» поля. Для внешнего API это может быть нормально; для внутреннего — нет. Безопасность через сокрытие не заменяет контролей, но отключение интроспекции снижает утечки. См. Wallarm и документацию вашей реализации; при необходимости фильтруйте доступ по ролям.

Без интроспекции поля всё равно можно угадывать; GraphQL может подсказывать похожие имена (Did you mean "user?") — при возможности отключите; не все движки это умеют. См. Shapeshifter и доклад.

Отключение интроспекции (Java)

GraphQLSchema schema = GraphQLSchema.newSchema()
    .query(StarWarsSchema.queryType)
    .fieldVisibility( NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY )
    .build();

Отключение интроспекции и GraphiQL (JavaScript)

app.use('/graphql', graphqlHTTP({
  schema: MySessionAwareGraphQLSchema,
  validationRules: [NoIntrospection],
  graphiql: process.env.NODE_ENV === 'development',
}));

Не раскрывайте лишнее в ошибках

В продакшене не отдавайте stack trace и не держите отладочный режим. В Apollo Server: debug: false или NODE_ENV в production/test. Внутренний лог стека без отдачи клиенту — masking and logging errors, общая документация по ошибкам.

Другие материалы

Инструменты

  • InQL Scanner — сканер безопасности GraphQL; генерация запросов/мутаций по схеме.
  • GraphiQL — обзор схемы и объектов.
  • GraphQL Voyager — визуализация схемы.

Практики и документация

Дополнительно об атаках

© Перевод на русский язык. Оригинальные материалы: OWASP Cheat Sheet Series.
Этот проект использует материалы OWASP, распространяемые по лицензии CC BY-SA 4.0.