Эта статья была первоначально опубликована на blog.escape.tech.
Zip-бомбы ушли в прошлое, но концепция, лежащая в их основе, по-прежнему актуальна в наши дни. Действительно, ваше GraphQL-приложение может быть уязвимо к тому, что в этой статье мы будем называть GraphQL-бомбами. Читайте дальше, чтобы узнать, уязвимы ли вы и как защитить свое GraphQL-приложение!
Как работают почтовые бомбы?
Прежде чем погрузиться в эту тему, давайте уделим немного времени пониманию концепции, лежащей в основе zip-бомб.
Zip-файлы — это сжатые без потерь архивы, наиболее распространенный алгоритм сжатия которых называется deflate. Он работает путем поиска повторяющихся шаблонов в данных, а затем заменяет эти шаблоны гораздо более короткими маркерами.
Таким образом, повторяющаяся последовательность байтов после сжатия становится намного короче. Создание zip-бомбы заключается в тщательном составлении последовательности байтов, которые очень хорошо сжимаются, на несколько порядков. Затем, когда жертва распаковывает zip-файл, полученные данные будут намного больше, чем исходный архив.
Псевдослучайные запросы
GraphQL — это мощный язык с множеством малоизвестных особенностей. Одной из таких возможностей является возможность псевдозапросов.
Рассмотрим простой блог с запросом article(id: Int!)
. Если бы нам нужно было получить одну статью, мы бы сделали это следующим образом:
query {
article(id: 1) {
title
author
}
}
Получится что-то вроде этого:
{
"article": {
"title": "Hello World!",
"author": "John Doe"
}
}
Но в GraphQL есть хорошая функция, позволяющая разработчикам запрашивать один и тот же резолвер несколько раз с разными именами возврата:
query {
first: article(id: 1) { title author }
second: article(id: 2) { title author }
third: article(id: 3) { title author }
}
Это даст аналогичный результат, но article
теперь будет псевдонимом first
, second
и third
:
{
"first": {
"title": "Hello World!",
"author": "John Doe"
},
"second": {
"title": "Yay, second article!",
"author": "Jane Doe"
},
"third": {
"title": "That's a lot of articles",
"author": "Jaune D'œuf"
}
}
Возможно, вы уже что-то заметили, и действительно, вы уже можете разработать первую уязвимость для использования этой особенности — алиасинг облегчает перебор:
mutation {
a1: login(user: "john", password: "password") { id }
a2: login(user: "john", password: "qwerty") { id }
a3: login(user: "john", password: "123456") { id }
# Let's try the most common passwords!
}
Загрузка файлов
Вторая часть уязвимости требует включения функции загрузки файлов через GraphQL.
Многие популярные движки GraphQL поддерживают загрузку файлов в GraphQL, а некоторые даже нативно.
Спецификация GraphQL multipart описывает, как реализовать загрузку файлов в GraphQL. Если обычные запросы GraphQL отправляются в виде application/json
, то загрузка файлов отправляется в виде multipart/form-data
. Это означает, что тело HTTP-запроса состоит из нескольких частей, и их функции, описанные в спецификации, можно свести к следующему:
- Операционная часть содержит GraphQL-запрос. Это та часть, которая обычно отправляется в виде
application/json
. - Часть map помогает серверу найти данные в теле запроса.
- Любая другая часть может быть использована в части операций, если она правильно отображена. Эти части могут содержать любой тип данных, с которыми может работать сервер, но обычно это изображения или двоичные данные.
Вот как выглядит загрузка фотографии профиля:
POST /graphql HTTP/1.1
Connection: keep-alive
Content-Length: 78346
Content-Type: multipart/form-data; boundary=----boundaryMGv2RzA6GpOE3Hry
Host: example.com
------boundaryMGv2RzA6GpOE3Hry
Content-Disposition: form-data; name="operations"
{
"query": "mutation ($picture: File!) {updateUserPicture(picture: $picture)}",
"variables": { "picture": null }
}
------boundaryMGv2RzA6GpOE3Hry
Content-Disposition: form-data; name="map"
{
"file1": ["variables.picture"]
}
------boundaryMGv2RzA6GpOE3Hry
Content-Disposition: form-data; name="file1"; filename="gautier.jpg"
Content-Type: image/jpeg
(77 kB of binary data)
------boundaryMGv2RzA6GpOE3Hry--
Здесь видно, что часть, имеющая name="file1"
, сопоставлена с variables.picture
, что позволяет серверу найти файл в теле запроса.
Загрузка файлов в GraphQL работает почти так же, как и загрузка файлов в REST, по крайней мере, с точки зрения HTTP, а это значит, что уязвимости, существующие в REST, могут быть использованы в GraphQL. Но, к сожалению, это еще не все…
Бомбы GraphQL
Как вы уже догадались, GraphQL bombs объединяет две предыдущие функции, представленные в статье. Концепция заключается в следующем: обращение к одному и тому же файлу несколько раз с помощью псевдослучайных запросов.
Представим, что мы вызываем updateUserPicture(picture: File!)
тысячу раз, используя псевдонимы, причем все вызовы ссылаются на один и тот же файл размером 1 МБ:
mutation {
a1: updateUserPicture(picture: $picture)
a2: updateUserPicture(picture: $picture)
a3: updateUserPicture(picture: $picture)
# ...
a1000: updateUserPicture(picture: $picture)
}
Этот запрос будет меньше 2 МБ, но в результате серверу придется обработать 1 ГБ данных.
В зависимости от того, что сервер делает с данными, этот запрос может вызвать истощение памяти или процессора, что приведет к снижению производительности или даже сбою сервера.
Смягчение последствий
Для устранения этой уязвимости необходимо предпринять несколько шагов:
- Правильно настройте ограничения сервера на загрузку файлов.
- Для Apollo с graphqlUploadExpress
- Для GraphQL Yoga
- Ограничьте использование пакетной обработки и алиасинга с помощью GraphQL Armor, проекта с открытым исходным кодом, разработанного Escape — GraphQL Security для устранения наиболее распространенных уязвимостей GraphQL.
- Если GraphQL Armor пока не поддерживает ваш движок, вы также можете попробовать graphql-no-batched-queries и graphql-no-alias.