SQL-инъекции: механика атак, типы и методы защиты с примерами

Они всё ещё в топе уязвимостей

SQL-инъекции: механика атак, типы и методы защиты с примерами

Если вы думаете, что современные хакеры ломают сайты через какие-то удивительные нейросети, то нет. На практике одна из самых опасных и до сих пор актуальных уязвимостей — SQL-инъекции. Они входят в OWASP Top 10 с момента появления списка и стабильно остаются в топе реальных угроз. По разным отчетам безопасности, значительная доля уязвимостей в популярных CMS и плагинах до сих пор связана именно с SQLi.

Суть атаки в том, что злоумышленник вставляет в пользовательский ввод — например, в строку поиска или форму логина — вредоносный фрагмент SQL-кода, который база данных исполняет как легитимную команду.

Последствия могут быть разные: от обхода аутентификации и получения доступа к аккаунтам, до утечки целых таблиц с пользовательскими данными, удаления информации или модификации записей. А в некоторых случаях — и выполнение системных команд на самом сервере, например, через уязвимость функции COPY FROM PROGRAM в PostgreSQL.

Сегодня разберём механику подобных эксплойтов, какие есть типы атак, посмотрим на реальные примеры уязвимого кода и научимся выстраивать защиту.

Как работает SQL-инъекция — механика атаки

Чтобы понять, как вас будут ломать, нужно посмотреть на свой код глазами атакующего. Классическая SQL-инъекция рождается в тот момент, когда разработчик решает «просто склеить» строку запроса и данные от пользователя.

Посмотрим на типичный, чудовищно уязвимый код на PHP:

# ❌ ОПАСНО: конкатенация пользовательского ввода в SQL
$id = $_GET['id'];
$query = "SELECT title, content FROM articles WHERE id = " . $id;
$result = $db->query($query);

Если обычный пользователь откроет site.com/article?id=5, то то итоговый SQL-запрос будет:

SELECT title, content FROM articles WHERE id = 5

Всё работает, как и ожидается. Но что, если хакер передаст в URL следующий параметр: 

?id=5 OR 1=1 —

Тогда наш PHP-скрипт склеит строку, и база данных получит вот это:

SELECT title, content FROM articles WHERE id = 5 OR 1=1 —

Разберём, как эти спецсимволы только что сломали вашу логику:

  • OR 1=1 — это безусловно истинное условие. База данных проверяет каждую строку: «Её ID равен 5? Нет. А 1 равно 1? Да!». В результате фильтр по id фактически перестаёт иметь значение, и запрос начинает возвращать все строки из таблицы.
  • (или # в MySQL) — это символ комментария. Он говорит базе данных: «Всё, что идёт после меня, игнорируй». Это позволяет хакеру «отрезать» остаток вашего легитимного запроса, чтобы он не вызвал синтаксическую ошибку.
  • ; (точка с запятой) — символ завершения инструкции. С его помощью можно приклеить к вашему SELECT совершенно другой запрос, например UPDATE или DROP.

При этом точки входа для атаки не ограничиваются только URL-параметрами ($_GET). Уязвимые данные могут приходить из любых источников ввода: из полей форм авторизации ($_POST), поддельных куки, и даже из HTTP-заголовков, например, User-Agent или Referer, если вы сохраняете их в БД для аналитики.

Типы SQL-инъекций

Специалисты по кибербезопасности разделяют типы SQL-инъекций на несколько категорий, в зависимости от того, как именно хакер получает украденные данные обратно.

In-Band / Union-based 

Самый простой и разрушительный вектор атаки.

Атакующий добавляет к вашему запросу оператор UNION SELECT, и база данных возвращает украденную информацию прямо на экран, в том же самом HTTP-ответе.

Пример запроса:

?id=-1 UNION SELECT username, password FROM admin —

В этом случае атакующий передаёт id=-1, чтобы исходный запрос гарантированно ничего не вернул. Дальше срабатывает UNION SELECT, который «склеивает» результат с таблицей admin.

В итоге приложение может отобразить логины и хеши паролей вместо обычного контента страницы.

⚠️ Важное условие: для успешного UNION количество и типы колонок в обоих SELECT должны совпадать с исходным запросом. Поэтому в реальных атаках часто сначала происходит «разведка» структуры запроса.

Error-based 

Этот тип работает там, где приложение не показывает данные напрямую, но оставляет включенными ошибки базы данных.

В таком случае атакующий использует конструкции, которые специально вызывают ошибку, а полезная информация «просачивается» в текст сообщения об ошибке.

Пример запроса для MySQL:

?id=1 AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT@@version)))

Хакер использует XML-функцию EXTRACTVALUE(), отдавая ей заведомо невалидный синтаксис. База падает с ошибкой, пытаясь вывести то, что её сломало. На экран пользователя выводится: XPATH syntax error: ‘~8.0.35-0ubuntu0’

И всё, версия базы раскрыта.

Blind Boolean-based 

Слепая SQL-инъекция. Используется в случаях, когда отключены сообщения об ошибках, а прямой вывод данных, например, через UNION, недоступен.

Данные не возвращаются напрямую, поэтому атакующий задаёт базе данных вопросы в формате «Да/Нет» и делает выводы по поведению приложения: например, страница загрузилась нормально или отдала пустой шаблон.

Пример запроса:

?id=5 AND SUBSTRING((SELECT password FROM admin LIMIT 1), 1, 1) = ‘a’ —

Хакер проверяет гипотезу: «Первая буква пароля админа — это ‘a’?». Если страница загрузилась с контентом (условие TRUE), значит буква угадана. Если нет — он пробует ‘b’, ‘c’ и так далее. Это посимвольный перебор. 

Атака очень медленная, генерирует тысячи запросов, но остаётся эффективной в системах без явных утечек ошибок.

Blind Time-based 

Более «тихий» вариант слепой инъекции, где результат определяется не содержимым ответа, а временем его получения.

Пример запроса:

?id=5 AND IF(
  SUBSTRING((SELECT password FROM admin LIMIT 1), 1, 1) = 'a',
  SLEEP(5),
  0
) --

Хакер задаёт тот же вопрос про букву пароля. Но теперь если буква угадана верно, база выполнит команду SLEEP(5) и сервер «зависнет» на 5 секунд. Если ответ пришёл мгновенно — буква не та. 

На практике такие атаки чаще всего выявляются по аномалиям в логах и системах мониторинга, где видны регулярные задержки запросов с одинаковыми паттернами.

Успейте: скидка 15% до 29 мая

Если вам интересно копаться в чужом коде, находить такие уязвимости или строить архитектуру, в которой их нет изначально, — держите промокод Практикума на любой платный курс: по ссылке (можно просто нажать). Даёт скидку 15% при покупке, плюс дополнительно 5 мини-курсов и 5 книг стоимостью ~75 000 ₽. Но только до 29 мая — потом всё, промокод сгорит.

Реальный сценарий атаки — обход аутентификации

Чтобы закрепить понимание, разберём один из самых классических сценариев SQL-инъекции — обход авторизации. Такие уязвимости до сих пор встречаются в легаси-приложениях, старых админках и корпоративных системах, где логика логина построена через «склейку» SQL-строк.

У нас есть простая уязвимая форма входа на PHP:

$user = $_POST['username'];
$pass = $_POST['password'];
// ❌ ОПАСНО: конкатенация пользовательских данных в SQL
$query = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'";
$result = $db->query($query);
if ($result->num_rows > 0) {
    login_user();
}

Теперь посмотрим, как происходит атака.

Злоумышленник оставляет поле пароля пустым, а в поле username вводит следующую строку: admin’ — .

После подстановки в запрос он превращается в:

SELECT * FROM users WHERE username = ‘admin’ — ‘ AND password = ”

Одинарная кавычка после admin закрывает строковый литерал логина. А два дефиса — превращают всю проверку пароля (AND password = ”) в неисполняемый комментарий, который игнорируется SQL-движком! 

В результате база данных фактически выполняет упрощённый запрос:

SELECT * FROM users WHERE username = ‘admin’

Если пользователь с таким логином существует, то система считает авторизацию успешной и выполняет login_user() — без проверки пароля вообще.

Защита от SQL-инъекций

Есть много способов борьбы с атаками, но защита от SQL-инъекций должна строиться комплексно. Разберём пять независимых методов защиты, от фундаментальных до вспомогательных.

Prepared Statements (параметризованные запросы) 

Основной, фундаментальный и наиболее надёжный метод защиты. По сути, все остальные подходы — лишь дополнительные слои. 

Логика вот в чем: SQL-запрос и пользовательские данные передаются в базу данных раздельно. Сначала СУБД «компилирует» структуру запроса, а затем подставляет значения параметров. При этом данные никогда не интерпретируются как SQL-код, даже если содержат кавычки, операторы или ключевые слова.

Было (уязвимо):

$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
$result = $db->query($sql);

Стало (безопасно, через PDO):

$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->bindParam(':email', $_POST['email']);
$stmt->execute();

Здесь мы применили PDO (PHP Data Objects) — встроенный слой работы с базой данных в PHP. Он выступает как прослойка между кодом приложения и СУБД и позволяет выполнять SQL-запросы безопасным способом через параметризацию.

Разница в том, что в этом варианте запрос и данные разделены:

  • SQL-запрос передаётся как шаблон (:email);
  • пользовательский ввод передаётся отдельно как значение параметра.

База данных сначала разбирает и фиксирует структуру запроса, а затем подставляет данные только как значения, не интерпретируя их как SQL-код. Поэтому даже если пользователь введёт test@example.com’ OR 1=1 —, это будет обычная строка, а не часть запроса.

ORM 

ORM (Object-Relational Mapping) — это слой, который позволяет работать с базой данных не через SQL, а через объекты и методы языка программирования. Например, Django ORM (Python), SQLAlchemy, Hibernate (Java), Eloquent (PHP).

Главная идея ORM в контексте безопасности в том, что он сам формирует SQL-запросы и автоматически параметризует данные, поэтому пользовательский ввод не попадает в SQL как код.

Проще говоря, ORM — это «переводчик», который не даёт случайно собрать опасный SQL руками.

Безопасный вариант в Django ORM:

user = User.objects.filter(username=user_input).first()

Здесь user_input просто передаётся как значение. ORM сам превращает это в безопасный SQL-запрос с параметрами.

А вот смертельно опасный запрос в Django ORM:

query = "SELECT * FROM auth_user WHERE username = '%s'" % user_input
user = User.objects.raw(query)

В этом случае ORM фактически обходится стороной: разработчик вручную собирает SQL-строку. И как только мы начинаем склеивать SQL руками — то возвращаемся к той же уязвимости, как будто ORM вообще не существует.

Валидация и whitelist 

Бывают ситуации, когда параметризовать запрос невозможно: например, нельзя параметризовать имена таблиц или столбцы в операторе ORDER BY. В таких случаях используется whitelist (белый список) — когда мы заранее определяем набор допустимых значений и разрешаем только их. Если значение не из списка — оно автоматически считается недопустимым.

Другой важный слой — проверка типов данных. Все числовые параметры, например, id должны приводиться к числу до попадания в запрос. Это убирает возможность вставить туда SQL-выражение.

Но при этом валидация — это не замена prepared statements, а только дополнительный фильтр.

Безопасная сортировка:

$allowed_orders = ['ASC', 'DESC'];
// Берём направление сортировки из URL (?dir=ASC|DESC)
// и пропускаем только значения из whitelist
$order = in_array(strtoupper($_GET['dir']), $allowed_orders)
    ? $_GET['dir']
    : 'ASC';

Теперь переменную $order безопасно вставлять в SQL

Минимальные привилегии БД 

Принцип минимальных привилегий гласит, что база данных должна работать с максимально ограниченными правами. Аккаунт приложения не должен иметь админских возможностей в базе:

  • удаление таблиц (DROP TABLE)
  • доступ к файловой системе (FILE)
  • выдача прав другим пользователям (GRANT)

В идеале приложению выдаются только необходимые операции: SELECT, INSERT, UPDATE — и только к конкретным таблицам.

Даже если SQL-инъекция произошла, ущерб ограничен тем, что разрешено этому пользователю БД. У злоумышленника не получится удалить всю базу или получить полный контроль над сервером.

WAF 

Web Application Firewall (WAF) — это защитный слой между пользователем и приложением. Например, ModSecurity, AWS WAF, Cloudflare. Он анализирует входящие запросы и пытается блокировать подозрительные паттерны атак вроде попыток вставить UNION SELECT или другие известные SQL-инъекционные конструкции.

Но важно, что WAF — это не замена безопасному коду, а лишь дополнительный эшелон защиты. Любой фильтр обходится через обфускацию — то есть, хитрое кодирование запросов, смену регистра, использование нестандартных пробельных символов. 

Опытный атакующий WAF обойдёт, поэтому уповать только на WAF нельзя.

Обнаружение уязвимостей и тестирование

Мало написать безопасный код, важно уметь его проверять. Автоматизированное и ручное тестирование SQL-инъекций должно быть регулярной практикой команды перед каждым деплоем на продакшен — это часть подхода DevSecOps, где безопасность встраивают в процесс разработки.

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

Стандартный первый шаг любого безопасника: попытаться сломать синтаксис.

Вы просто добавляете одинарную кавычку к любому параметру в URL (?id=5′) и наблюдаете за поведением системы. Если страница упала с 500-й ошибкой, изменился вывод контента или время ответа резко возросло — перед вами потенциальная уязвимость. 

Подробные векторы проверок можно найти в официальном руководстве OWASP по тестированию.

Правовая оговорка: тестировать на уязвимости можно только собственные приложения, либо проекты, официально размещенные на платформах Bug Bounty (HackerOne, Bugcrowd). Любые тесты на чужих сайтах без разрешения незаконны.

Автоматизированные инструменты 

Сканировать всё руками долго, поэтому инженеры используют специализированный софт. Самый известный из них — консольная утилита sqlmap

Базовая команда sqlmap выглядит так:

sqlmap -u “http://site.com/page?id=1” –dbs

Она автоматически проверит сотни векторов атак на указанный параметр id и выведет список всех доступных баз данных (–dbs), если найдет уязвимость. 

В корпоративном секторе чаще используют комбайны вроде Burp Suite Scanner, который автоматически анализирует HTTP-запросы через прокси и проверяет их на типовые уязвимости.

⚠️ Такие инструменты — это полноценное «пентест-оружие».  Он используется только в разрешённых пентестах и аудите своего кода. Несанкционированное применение по чужим серверам — это уголовная ответственность.

Вы не спрашивали, но мы ответим

Можно ли защититься только с помощью WAF? 

Нет. 

WAF работает на основе сигнатур и фильтрует только известные паттерны атак. Проблема в том, что SQL-инъекции легко маскируются: меняется регистр, кодировка, структура запроса — и фильтр перестаёт их узнавать. WAF может снизить шум и отсеять часть автоматических атак, но не заменяет исправление уязвимостей в коде.

Чем prepared statements лучше экранирования (mysqli_real_escape_string)?

Экранирование просто добавляет слэши перед кавычками, пытаясь «обезвредить» строку.

Но это всегда игра в догонялки: сложные кодировки и нестандартные сценарии могут ломать такую защиту. Prepared statements работают иначе — SQL-запрос и данные разделены на уровне протокола. База данных физически не воспринимает входные данные как код, только как значения.

Как обнаружить SQLi в legacy-коде? 

Обычно используют комбинацию подходов: статический анализ кода (SAST) для поиска конкатенации строк, ручной аудит критичных мест (логины, фильтры, поиск), и динамическое тестирование через инструменты по типу Burp Suite или sqlmap на тестовом стенде. В старых проектах почти всегда находятся места, где SQL собирается вручную.

Влияет ли использование ORM на производительность по сравнению с «сырыми» запросами? 

Да, ORM добавляет небольшой оверхед на конвертацию объектов в SQL и обратно, что может замедлить работу на сверхнагруженных узлах. Но для 99% задач эта разница микроскопическая. В узких местах под высокой нагрузкой «сырые» запросы могут быть быстрее, но на практике выигрывают не миллисекунды, а безопасность и скорость разработки. ORM снижает риск SQL-инъекций за счёт автоматической параметризации.

Заключение

Классические SQL-инъекции опасны не потому, что их сложно понять или они требуют гениальных хакерских навыков. Они процветают десятилетиями только из-за одной плохой привычки разработчиков — вставлять пользовательский ввод напрямую в тело запроса. 

Как только вы убираете конкатенацию строк и переходите на prepared statements или корректный ORM, большая часть таких уязвимостей исчезает автоматически. Всё остальное — уже дополнительные слои защиты: валидация, минимальные привилегии, WAF и мониторинг.

Именно поэтому SQLi до сих пор встречается не из-за сложности атаки, а из-за банальной привычки писать SQL как строку.

Советуем дополнительно почитать по теме: 

Бонус для читателей

Если вам интересно погрузиться в мир IT и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.

Вам может быть интересно
medium