SQL-инъекции на учебном стенде
Учимся видеть SQL-инъекцию в коде и закрывать её параметризованными запросами — на безопасном стенде, чтобы потом грамотно защищать продакшен.
SQL-инъекция (SQLi) — уязвимость, при которой ввод пользователя попадает в текст SQL-запроса как код, а не как данные, и позволяет изменить смысл запроса.
Этот урок — практическая работа на учебном стенде (DVWA в режиме low/medium или OWASP Juice Shop, развёрнутые в локальной виртуальной машине). Мы не атакуем чужие сайты: SQLi мы изучаем, чтобы научиться находить и закрывать её в собственном коде. Несанкционированный доступ к чужой базе данных — это статья 272 УК РФ, и никакой «интерес» этого не оправдывает. Тренируемся только там, где разрешено.
Зачем это знать защитнику
SQL-инъекция десятилетиями держится в верхушке рейтинга OWASP Top 10 (категория Injection). Одна уязвимая строка может отдать злоумышленнику всю таблицу пользователей с хешами паролей. Разработчик, который понимает механику атаки, пишет код, в котором инъекция невозможна в принципе. Специалист Blue Team, который понимает её, умеет настроить мониторинг и распознать попытку эксплуатации в логах. Поэтому начинаем с понимания, как уязвимость вообще появляется.
Как возникает уязвимость: конкатенация запроса
Корень проблемы один — строку запроса склеивают из шаблона и пользовательского ввода. Сервер потом не различает, где «структура запроса», а где «данные»: для него это единый текст. Классический уязвимый код авторизации:
# УЯЗВИМО: ввод склеивается в текст запроса
login = request.form["login"]
password = request.form["password"]
query = "SELECT id FROM users WHERE login = '" + login + "' AND password = '" + password + "'"
cursor.execute(query)
Пока в login приходит обычное имя вроде alice, всё работает. Но ввод — это часть текста запроса. Если в поле логина оказывается строка, содержащая одинарную кавычку и логическое условие, она закрывает кавычку шаблона и дописывает свою логику. На стенде в учебном поле это выглядит так:
login: admin' --
password: (любое)
Запрос превращается в ... WHERE login = 'admin' -- ' AND password = '...'. Последовательность -- в SQL — это начало комментария, поэтому проверка пароля просто отбрасывается. Это и есть суть инъекции: ввод изменил структуру запроса. Заметьте — мы разобрали принцип, а не «рецепт взлома сайта»: цель в том, чтобы вы узнавали такую конкатенацию в своём коде и убирали её.
Как это находят на CTF
На CTF и в легальном пентесте первый сигнал инъекции — реакция приложения на спецсимволы. Исследователь подставляет в поле одиночную кавычку и смотрит: появилась ошибка SQL ("unterminated string", "syntax error near") — значит, ввод дошёл до интерпретатора без экранирования. Дальше для подтверждения сравнивают поведение на логически истинном и ложном условии (boolean-based): если страница реагирует по-разному, инъекция подтверждена. Автоматизируют разведку сканерами вроде sqlmap — строго против своего стенда:
# Только против СВОЕГО учебного стенда (например, DVWA на localhost)
sqlmap -u "http://localhost/vulnerabilities/sqli/?id=1" --batch --level=1
Инструмент здесь — иллюстрация метода: он подставляет пробные строки и анализирует отклик. На реальной чужой системе такой запуск — уже правонарушение.
Как это работает под капотом
База данных получает строку и проходит её через парсер, который строит дерево разбора (какие таблицы, условия, операторы). Парсер не знает, что часть текста «пришла от пользователя» — он видит единый запрос и честно исполняет всё, что распарсил. Поэтому защита не может быть «отфильтровать плохие слова»: список обхода бесконечен (разный регистр, кодировки, комментарии внутри ключевых слов). Правильное решение — отделить код запроса от данных на уровне протокола, чтобы данные физически не могли стать кодом. Это и делают параметризованные запросы.
Как защититься
1. Параметризованные запросы (prepared statements) — главная защита. Текст запроса с плейсхолдерами отправляется в СУБД отдельно от значений. Драйвер передаёт значения как данные мимо парсера — кавычка внутри значения остаётся обычным символом и не закрывает строку. Тот же логин-сценарий, но безопасно:
# БЕЗОПАСНО: структура и данные разделены плейсхолдерами
login = request.form["login"]
password = request.form["password"]
cursor.execute(
"SELECT id FROM users WHERE login = %s AND password = %s",
(login, password),
)
Теперь даже ввод admin' -- ищется как буквальный логин с такими символами — и просто не находится. Инъекция невозможна, потому что значение никогда не парсится как SQL.
2. ORM по умолчанию параметризует. Django ORM, SQLAlchemy и подобные строят запросы безопасно. Опасность возвращается, только если вы вручную склеиваете «сырой» SQL:
# БЕЗОПАСНО: ORM сам параметризует
User.objects.filter(login=login, password_hash=ph)
# ОПАСНО даже внутри ORM — снова конкатенация:
User.objects.raw("SELECT * FROM users WHERE login = '" + login + "'")
3. Валидация и принцип наименьших привилегий — слои в глубину. Проверяйте формат ввода (например, числовой id приводите к int — тогда строковая инъекция в него невозможна). Заводите для приложения отдельного пользователя БД без прав DROP/GRANT: даже при ошибке в коде ущерб ограничен. Храните пароли как хеши (bcrypt/argon2) — тогда утечка таблицы не отдаёт пароли в открытом виде.
4. Обнаружение. WAF и аномалии в логах (всплеск ошибок SQL, спецсимволы в параметрах, странные UNION/--) — сигнал попытки эксплуатации. Это задача мониторинга, а параметризация — задача кода; работают они вместе.
Итоги
- SQL-инъекция возникает из конкатенации ввода в текст запроса: данные становятся кодом.
- На CTF её находят по реакции на спецсимволы и разному поведению при истинном/ложном условии — только на своём стенде.
- Главная защита — параметризованные запросы / ORM: они разделяют структуру и данные на уровне протокола.
- Дополнительно: валидация типов, минимальные права пользователя БД, хеширование паролей, мониторинг.
- Любая практика — только на DVWA/Juice Shop/своей ВМ. Чужая БД = ст. 272 УК РФ.