Пентест приложений с GraphQL
В последнее время GraphQL набирает всё большую популярность, а вместе с ней растёт и интерес со стороны специалистов информационной безопасности. Технологию используют такие компании, как: Facebook, Twitter, PayPal, Github и другие, а это значит, что пора разобраться, как тестировать такое API. В этой статье мы расскажем о принципах этого языка запросов и направлениях тестирования на проникновение приложений с GraphQL.
Зачем нужно знать GraphQL? Этот язык запросов активно развивается и всё больше компаний находят ему практическое применение. В рамках программ Bug Bounty популярность этого языка тоже растёт, интересные примеры можно посмотреть здесь, здесь и здесь.
Подготовка
Тестовая площадка, на которой вы найдете большинство примеров, приведённых в статье.
Список с приложениями, которые вы тоже можете использовать для изучения.
Для взаимодействия с различными API лучше использовать IDE для GraphQL:
Последнее IDE мы и рекомендуем: в Insomnia удобный и простой интерфейс, есть множество настроек и автодополнение полей запросов.
Перед тем, как перейти непосредственно к общим методам анализа безопасности приложений c GraphQL, вспомним основные понятия.
Что такое GraphQL?
GraphQL — язык запросов для API, призванный обеспечить более эффективную, мощную и гибкую альтернативу REST. В его основе лежит декларативная выборка данных, то есть клиент может точно указать, какие именно данные ему нужны от API. Вместо нескольких конечных точек API (REST) GraphQL представляет единую конечную точку, которая предоставляет клиенту запрашиваемые данные.
Основные различия между REST и GraphQL
Обычно в REST API вам необходимо получать информацию с разных конечных точек. В GraphQL для получения тех же данных вам необходимо сделать один запрос с указанием данных, которые вы хотите получить.
REST API предоставляет ту информацию, которую в API заложит разработчик, то есть в случае, если вам необходимо получить больше или меньше информации, чем предполагает API, то нужны будут дополнительные действия. Опять же, GraphQL выдаёт точно запрашиваемую информацию.
Полезным дополнением будет то, что в GraphQL есть схема, описывающая, как и какие данные клиент может получить.
Виды запросов
В GraphQL существует 3 основных вида запросов:
- Query
- Mutation
- Description
Query
Запросы Query используются для получения/чтения данных в схеме.
Пример такого запроса:
query {
allPersons {
name
}
}
В запросе указываем, что хотим получить имена всех пользователей. Помимо имени мы можем указать и другие поля: age, id, posts и др. Чтобы узнать, какие именно поля мы можем получить, нужно нажать Ctrl+Пробел. В данном примере мы передаём параметр, с которым приложение вернёт первые две записи:
query {
allPersons(first: 2) {
name
}
}
Mutation
Если тип query нужен для чтения данных, то тип mutation нужен для записи, удаления и изменения данных в GraphQL.
Пример такого запроса:
mutation {
createPerson(name:"Bob", age: 37) {
id
name
age
}
}
В этом запросе мы создаём пользователя с именем Bob и возрастом 37 (эти параметры передаются как аргументы), во вложении (фигурных скобках) указываем, какие данные мы хотим получить от сервера после создания пользователя. Это нужно для того, чтобы понять, что запрос выполнен успешно, а также для получения данных, которые сервер генерирует самостоятельно, такие как id.
Subscription
Ещё один вид запросов в GraphQL — subscription. Он нужен для оповещения пользователей о каких-либо изменениях, произошедших в системе. Работает это так: клиент подписывается на какое-то событие, после чего с сервером устанавливается соединение (обычно через WebSocket), и, когда это событие происходит, сервер отсылает клиенту уведомление по установленному соединению.
Пример:
subscription {
newPerson {
name
age
id
}
}
Когда создастся новый Person, сервер отошлёт клиенту информацию. Наличие запросов subscription в схемах встречается реже, чем query и mutation.
Стоит отметить, что все возможности по query, mutation и subscription создаёт и настраивает разработчик конкретной API.
Факультатив
На практике разработчики часто используют alias и OperationName в запросах для внесения ясности.
Alias
GraphQL для запросов предусматривает возможность alias, которая может облегчить понимание того, что именно запрашивает клиент.
Предположим, что у нас есть запрос вида:
{
Person(id: 123) {
age
}
}
который выведет имя пользователя с id 123. Пусть имя этого пользователя будет Vasya. Чтобы в следующий раз не ломать голову, что выведет данный запрос, можно сделать вот так:
{
Vasya: Person(id: 123) {
age
}
}
OperationName
Помимо alias в GraphQL используется OperationName:
query gettingAllPersons {
allPersons {
name
age
}
}
OperationName нужен для пояснения того, что именно делает запрос.
Пентест
После того, как мы разобрались с основами, переходим непосредственно к пентесту. Как понять, что приложение использует GraphQL? Вот пример запроса, в котором есть GraphQL-запрос:
POST /simple/v1/cjp70ml3o9tpa0184rtqs8tmu/ HTTP/1.1
Host: api.graph.cool
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: */*
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: https://api.graph.cool/simple/v1/cjp70ml3o9tpa0184rtqs8tmu/
content-type: application/json
Origin: https://api.graph.cool
Content-Length: 139
Connection: close
{"operationName":null,"variables":{},"query":"{\n __schema {\n mutationType {\n fields {\n name\n }\n }\n }\n}\n"}
Некоторые параметры, по которым можно понять, что перед вами GraphQL, а не что-то иное:
- в теле запроса имеются слова: __schema, fields, operationName, mutation и др.;
- в теле запроса много символов »\n». Как показывает практика, их можно убрать, чтобы было удобнее читать запрос;
- часто путь отправки запроса на сервер: ⁄graphql
Отлично, нашли и определили. Но куда вставлять кавычку как узнать, с чем нам нужно работать? На помощь придёт интроспекция.
Интроспекция
В GraphQL предусмотрена схема интроспекции, т.е. схема с описанием данных, которые мы можем получить. Благодаря этому мы можем узнать, какие запросы существуют, какие аргументы им можно/нужно передавать и многое другое. Заметим, что в отдельных случаях разработчики намеренно не разрешают возможность интроспекции их приложения. Тем не менее, основное большинство всё же оставляет такую возможность.
Рассмотрим основные примеры запросов.
Пример 1. Получение всех видов запросов
query {
__schema {
types {
name
fields {
name
}
}
}
}
Формируем запрос query, указываем, что хотим получить данные по __schema, а в ней типы, их имена и поля. В GraphQL существуют служебные имена переменных: __schema, __typename, __type
В ответе мы получим все типы запросов, их имена и поля, которые существуют в схеме.
Пример 2. Получение полей для конкретного вида запроса (query, mutation, description)
query {
__schema {
queryType {
fields {
name
args {
name
}
}
}
}
}
Ответом на данный запрос будут все возможные запросы, которые мы можем выполнить к схеме для получения данных (вид query), и возможные/необходимые аргументы для них. Для некоторых запросов указание аргумента (ов) обязательно. Если выполнить такой запрос без указания обязательного аргумента, сервер должен выдать сообщение с ошибкой, что необходимо его указать. Вместо queryType мы можем подставлять mutationType и subscriptionType для получения всех возможных запросов по мутациям и подпискам соответственно.
Пример 3. Получение информации о конкретном типе запроса
query {
__type(name: "Person") {
fields {
name
}
}
}
Благодаря такому запросу мы получим все поля для типа Person. В качестве аргумента вместо Person мы можем передавать любые другие имена запросов.
Теперь, когда мы можем разобраться с общей структурой тестируемого приложения, давайте определим, что же мы будем искать.
Information disclosure
Чаще всего приложение, использующее GraphQL, состоит из множества полей и типов запросов, и, как известно многим, чем сложнее и больше приложение, тем сложнее его настраивать и следить за его безопасностью. Именно поэтому при тщательной интроспекции можно найти что-нибудь интересное, например: ФИО пользователей, номера их телефонов и другие критичные данные. Поэтому если вы хотите найти что-то подобное, то рекомендуем проверять все возможные поля и аргументы приложения. Так в рамках пентеста в одном из приложений были обнаружены данные пользователей: ФИО, номер телефона, дата рождения, некоторые данные карт и пр.
Пример:
query {
User(id: 1) {
name
birth
phone
email
password
}
}
Перебирая значения id, мы сможем получить информацию о других пользователях (а, может, и нет, если всё настроено правильно).
Injections
Стоит ли говорить, что практически везде, где есть работа с большим объёмом данных, есть и базы данных? А где есть БД — там могут быть и SQL-injections, NoSQL-injections и другие виды инжектов.
Пример:
mutation {
createPerson(name:"Vasya'--+") {
name
}
}
Здесь элементарная SQL-инъекция в аргументе запроса.
Authorization bypass
Допустим, мы можем создавать пользователей:
mutation {
createPerson(username:"Vasya", password: "Qwerty1") {
}
}
Предположив, что есть некий параметр isAdmin в обработчике на сервере, мы можем отправить запрос вида:
mutation {
createPerson(username:"Vasya", password: "Qwerty1", isAdmin: True) {
}
}
И сделать пользователя Vasya администратором.
DoS
Помимо заявленного удобства в GraphQL есть и свои недочеты с точки зрения безопасности. Рассмотрим пример:
query {
Person {
posts {
author {
posts {
author {
posts {
author ...
}
}
}
}
}
}
}
Как видите, мы создали зацикленный вложенный запрос. При большом количестве таких вложений, например, в 50 тыс., мы можем отправить запрос, который будет очень долго обрабатываться сервером или вообще «уронит» его. Вместо обработки валидных запросов сервер будет занят распаковкой гигантской вложенности запроса-пустышки.
Помимо большой вложенности, запросы сами по себе могут быть «тяжелыми» — это когда у одного запроса масса полей и внутренних вложений. Такой запрос тоже может вызвать затруднения в обработке на сервере.
Вывод
Итак, мы рассмотрели основные принципы тестирования на проникновение приложений с GraphQL. Надеемся, вы узнали что-то новое и полезное для себя. Если вам интересна данная тема, и вы хотите изучить её глубже, то рекомендуем следующие ресурсы:
И не забывайте: practice makes perfect. Удачи!