CoderCastrov logo
CoderCastrov
ГрафКьюЭль

Защита от атак с использованием больших запросов GraphQL

Защита от атак с использованием больших запросов GraphQL
просмотров
7 мин чтение
#ГрафКьюЭль

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

Как работает атака

Скраперы являются неотъемлемой частью веба. Они делают все возможное, чтобы извлечь информацию с вашего сайта для своих собственных целей. Впрочем, скраперы не всегда плохи. Ведь GoogleBot, который индексирует сайты для поиска Google? Это скрапер. Но скраперы могут быть злоумышленными или наносить вред вашему сайту. Они хотят получить все данные, которые ваш сайт может предложить, но у них нет прямого доступа к вашей базе данных. Поэтому они хотят опрашивать данные так часто, как только могут, что может привести к увеличению счетов за сервер или вызвать проблемы с качеством обслуживания для ваших пользователей.

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

GraphQL идеально подходит для этого: это гибкий язык запросов, который позволяет злоумышленникам создавать любые запросы, которые они хотят.

Допустим, вы работаете на сайте, где есть пользователи, и пользователи могут продавать товары. Многие сайты соответствуют этому формату: Amazon, Gumroad и т. д. "Дропшипперы" были бы рады знать цену каждого товара на сайте, чтобы продавать товары дороже в других местах и оставлять себе разницу. Например:

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

query {
    users {
      items_for_sale {
        name
        description
        price_usd
      }
    }
}

Хорошо, вы не родились вчера. Конечно, вы не разрешаете запрашивать каждого пользователя из одного поля. И вот здесь начинаются хитрые атаки: что, если пользователи просто запрашивают товары для каждого отдельного пользователя?

query {
  # Запрос данных для пользователя 1
  user_1: user(id: 1) {
    items_for_sale(first: 100) {
      name
      description
      price
    }
  }  # Запрос данных для пользователя 2
  user_2: user(id: 2) {
    items_for_sale(first: 100) {
      name
      description
      price
    }
  }  # ... пропущено несколько запросов  # Запрос данных для пользователя 10000
  user_10000: user(id: 10000) {
    items_for_sale(first: 100) {
      name
      description
      price
    }
  }
}

Они тщательно сконструировали запрос, который запрашивает данные для первых 10 000 пользователей. Конечно, они не получили ВСЕ объявления, так как они запросили только первые 100, но у большинства пользователей нет 100 объявлений. Они также могут отправлять дополнительные запросы пагинации для пользователей, которые возвращают 100 элементов.

Хорошо, вы исправили запрос, чтобы обработать это. Теперь вам нужно задаться вопросом: разрешает ли ваш сервер GraphQL объединять несколько запросов вместе? Многие клиентские фреймворки собирают запросы вместе и отправляют их все вместе. Apollo GraphQL позволяет такое объединение. Они называют это "пакетным объединением запросов" в своем блоге об этом.

[
  {
    query: <первый запрос>,
    variables: <переменные для первого запроса>
  },
  {
    query: <второй запрос>,
    variables: <переменные для второго запроса>
  } 
]

Наверняка вы можете представить, как это работает: вместо отправки одного огромного мегазапроса с 10 000 запросами вы можете отправить один HTTP-запрос с 10 000 пакетами.

И, наконец, у нас есть последнее измерение: злоумышленник может просто отправить 10 000 отдельных запросов отдельно.

Сохранение запросов

Одна из стратегий смягчения рисков - использование сохраненных запросов.

Обычно запросы GraphQL имеют две части: сам запрос и переменные.

В формате JSON пакет запроса может выглядеть так:

{
  "query": "query {
    user(id: $user_id) {
      name
    }
  }",
  "variables": {
    "user_id": 123 
  }
}

Однако это не единственный способ выполнения запросов GraphQL. Сервер может быть настроен на сохранение запросов GraphQL. Клиент больше не может указывать собственные запросы GraphQL. Вместо этого сам запрос определяется на сервере и ссылается клиентом.

Вот пример того, как может выглядеть такой запрос:

{
  "query": "SomeHashValueReferringToTheQuery",
  "variables": {
    "user_id": 123 
  }
}

На сервере обработчик будет искать "SomeHashValueReferringToTheQuery" и заменять его соответствующим запросом перед передачей запроса и переменных в GraphQL-движок.

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

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

Ограничение сложности запроса

Существует еще одно решение, которое намного сложнее, но дает гораздо лучшие результаты: ограничение сложности запроса.

В этой схеме вы указываете спецификацию для того, насколько "дорогим" является каждое поле вашего запроса. Например, может быть, что простое поле стоит 1 балл, а объект стоит 10 баллов. Сервер будет вычислять, сколько объектов запрашивается и стоимость каждого, и сравнивать это с порогом сложности запроса. Если порог превышает установленный предел, то сервер отклоняет запрос.

Вот пример того, как это работает:

query {
    # Всего 1310 баллов
    user(id: 1) { # 10 баллов      # Это выражение стоит 100 * (10 + 3) = 1300 баллов
      items_for_sale(first: 100) { # 10 баллов за каждый объект
                                   # плюс 1 за каждое поле
        name          # один балл
        description   # один балл
        price         # один балл
      }
    }
}

В целом, запрос стоит 1310 "баллов".

  • Каждое внутреннее поле стоит 1 балл
  • Чтение объекта для каждого items_for_sale стоит 10 баллов. Таким образом, каждый товар для продажи стоит 10 баллов плюс стоимость каждого из его полей, что составляет 13 баллов
  • Запрашивается 100 товаров для продажи, поэтому 1300 баллов.
  • Существует внешний пользователь, который запрашивается, что составляет 10 баллов.
  • Таким образом, запрос составляет 1310 баллов.

Если это самый сложный запрос, который вы планируете выполнить, то вы можете установить ограничение на одиночный запрос, например, 2000 баллов, чтобы оставить себе некоторую свободу для добавления запроса. Посмотрите, как ваш сервис работает при этой нагрузке и определите, нужно ли вам ужесточить это.

Вы даже можете объединить это с ролями пользователей и разрешить определенным пользователям выполнять более сложные запросы. Например, вашим сотрудникам можно разрешить выполнять запросы на 100 000 баллов, чтобы они могли выполнять крупномасштабный анализ, необходимый для их работы.

Расширенное ограничение скорости с учетом сложности запроса

Конечно, это не позволяет вам предотвратить атакующих, просто отправляющих несколько запросов подряд. Для этого нам нужно фактически хранить количество потребленных очков любым IP-адресом или пользовательским токеном в процессе выполнения запросов к API и предотвращать их использование в большом количестве в течение определенного временного интервала.

Например, предположим, что есть ограничение в 10 минут и 10 000 очков. Если вы выполняете запрос для получения данных для 100 пользователей, то у этого пользователя остается 8690 очков, и он может выполнить еще 6 таких запросов в течение следующих 10 минут, прежде чем его аккаунт будет ограничен.

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

Защитите себя с помощью универсального прокси-сервера для GraphQL

Не каждый фреймворк GraphQL предлагает функции для ограничения сложности запросов. Если вы находитесь в такой ситуации, вы можете использовать прокси-сервер, чтобы отклонять такие запросы от вашего имени.

Я являюсь независимым разработчиком программного обеспечения и работаю над прокси-сервером, способным справиться с этой задачей. Если вы не хотите заморачиваться с настройкой ограничителя скорости, который может вычислять сложность запроса и блокировать запросы на его основе, то я могу помочь вам. Первые 25 человек, зарегистрировавшихся, получат скидку 25% на первый год при запуске сервиса, а все остальные получат скидку 10% на первый год.

Узнать больше здесь