
SQL-инъекция в PHP возникает, когда пользовательский ввод напрямую вставляется в SQL-запрос без корректной фильтрации. В результате атакующий может изменить структуру запроса и получить доступ к данным, включая пароли, токены авторизации и финансовую информацию. Этот тип уязвимости до сих пор фиксируется в реальных проектах, особенно при использовании устаревших функций mysql_query и строковой конкатенации.
Надежная защита строится на применении подготовленных выражений (prepared statements) с привязанными параметрами. В PHP это реализуется через расширения PDO и MySQLi. При таком подходе SQL-запрос компилируется отдельно от значений, а переданные параметры автоматически экранируются на уровне драйвера базы данных, что исключает возможность внедрения кода.
Кроме подготовки запросов, критически важно ограничивать права пользователей базы данных. Аккаунт, к которому подключается PHP-приложение, не должен иметь права DROP или GRANT, если они не нужны. Это минимизирует последствия даже в случае удачной атаки. Дополнительно рекомендуется включать строгую валидацию входных данных – например, проверку формата email или диапазона числовых значений ещё до отправки в SQL.
Использование подготовленных выражений с PDO

PDO поддерживает параметризацию запросов, при которой данные никогда не вставляются в SQL-строку напрямую. Вместо конкатенации строк используются плейсхолдеры, что полностью исключает возможность внедрения произвольного кода.
Пример безопасного запроса:
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(['email' => $userEmail]);
$result = $stmt->fetchAll();
Вместо :email подставляется только значение, прошедшее экранирование и проверку драйвером. Это гарантирует, что даже при вводе символов кавычек или SQL-ключевых слов они не изменят структуру запроса.
Поддерживаются как именованные, так и позиционные плейсхолдеры. Для массовых операций можно использовать цикл:
$stmt = $pdo->prepare("INSERT INTO logs (user_id, action) VALUES (?, ?)");
foreach ($logs as $log) {
$stmt->execute([$log['user_id'], $log['action']]);
}
Рекомендуется отключать эмуляцию подготовленных выражений с помощью $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);, чтобы использовать нативные возможности СУБД.
Всегда проверяйте код возврата execute() и используйте bindParam или bindValue для явного указания типа данных при необходимости:
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = :id");
$stmt->bindValue(':id', $orderId, PDO::PARAM_INT);
$stmt->execute();
Такая практика предотвращает автоматическое преобразование типов и повышает предсказуемость работы приложения.
Правильная работа с mysqli и bind_param
Использование метода bind_param позволяет безопасно подставлять значения в SQL-запросы без риска выполнения произвольного кода. Этот метод работает только с подготовленными выражениями, поэтому запрос сначала компилируется сервером, а данные передаются отдельно.
Сигнатура метода: bind_param(string $types, mixed &$var1, mixed &$var2, ...). Аргумент $types определяет типы данных для каждого параметра: i – целое число, d – число с плавающей точкой, s – строка, b – бинарные данные. Количество символов должно соответствовать числу переданных переменных.
Пример безопасного запроса:
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
Необходимо проверять успешность подготовки выражения: if(!$stmt) { /* обработка ошибки */ }. Отсутствие этой проверки может привести к скрытым ошибкам и неконтролируемому поведению.
При работе с несколькими параметрами важно передавать их в том же порядке, что и в запросе. Например: $stmt->bind_param("si", $name, $age); для запроса WHERE username=? AND age=?.
Передача данных через bind_param исключает необходимость ручного экранирования. Использование real_escape_string в таких случаях избыточно и не должно применяться.
После выполнения выражения необходимо освобождать ресурсы: $stmt->close();. Это особенно важно при большом числе запросов в рамках одного соединения.
Фильтрация и валидация пользовательских данных до запроса
Любые данные из внешних источников должны проверяться до передачи в SQL. Минимальный уровень защиты – использование встроенных функций PHP для фильтрации. Например, filter_var($email, FILTER_VALIDATE_EMAIL) позволяет убедиться, что строка соответствует формату email. Для числовых значений применяют filter_var($id, FILTER_VALIDATE_INT), исключая ввод символов, несовместимых с целыми числами.
Регулярные выражения полезны для проверки ограниченных форматов: телефонных номеров, кодов, логинов. Важно задавать максимально строгие шаблоны, например preg_match('/^[0-9]{1,10}$/', $input) для идентификаторов фиксированной длины.
Опасно передавать необработанные строки в SQL даже после фильтрации. Фильтрация должна исключать заведомо неверные значения, а не «чистить» их. Если данные не проходят проверку, они должны быть отклонены или заменены на безопасное значение по умолчанию.
Валидация всегда должна учитывать контекст: дата проверяется через DateTime::createFromFormat(), URL через FILTER_VALIDATE_URL. Текстовые поля с произвольным содержимым лучше ограничивать по длине функцией mb_strlen(), предотвращая переполнение.
Фильтрация и валидация не заменяют параметризацию запросов, но существенно снижают риск попадания неожиданных значений в SQL, оставляя атакующему меньше возможностей для манипуляции.
Ограничение прав базы данных для минимизации рисков

Даже при использовании подготовленных выражений вредоносный запрос может нанести ущерб, если аккаунт базы данных обладает избыточными правами. Минимизация прав снижает масштаб возможной атаки.
- Создавайте отдельного пользователя БД только для веб-приложения, не используйте root или суперпользователя.
- Предоставляйте доступ только к конкретной схеме, без возможности обращаться к системным базам.
- Ограничьте права до уровня, необходимого для работы: SELECT, INSERT, UPDATE, DELETE. Исключите CREATE, DROP, ALTER, GRANT.
- Для операций, где требуется расширенный доступ (например, миграции), используйте отдельного пользователя, не связанного с веб-приложением.
- Регулярно проверяйте права командой
SHOW GRANTS FOR 'user'@'host';, удаляйте лишние. - При работе с файлами отключите FILE-права, чтобы злоумышленник не смог выгрузить данные или загрузить скрипт на сервер.
Жесткое разграничение прав превращает потенциальную SQL-инъекцию в локальный инцидент, не позволяя атакующему управлять структурой базы или выполнять системные команды.
Применение whitelisting для динамических параметров
При формировании SQL-запросов с динамическими элементами безопаснее не фильтровать «плохие» значения, а заранее задать список допустимых. Такой подход исключает вероятность ввода произвольных данных, не предусмотренных логикой приложения.
Пример: при выборе колонки для сортировки разрешайте только конкретные значения. Вместо передачи параметра напрямую используйте ассоциативный массив:
$allowed = [
'name' => 'name',
'date' => 'created_at',
'price' => 'price'
];
$sort = $_GET['sort'] ?? 'name';
$column = $allowed[$sort] ?? $allowed['name'];
$sql = "SELECT * FROM products ORDER BY $column";
Здесь пользовательский ввод не попадает в запрос без проверки. Даже если будет передано значение вне списка, подставится безопасное поле по умолчанию.
Whitelisting особенно важен для динамического выбора таблиц, колонок или направлений сортировки, где параметризованные запросы не применимы. Всегда ограничивайте набор разрешённых значений фиксированными массивами или константами, исключая прямое использование пользовательских данных.
Логирование и мониторинг попыток внедрения SQL кода

Эффективная защита от SQL инъекций невозможна без системного логирования и мониторинга. Каждая попытка вставки вредоносного кода должна фиксироваться для последующего анализа и предотвращения повторений.
В PHP рекомендуется использовать централизованное логирование запросов с подозрительными параметрами. Для этого удобно применять механизмы типа Monolog или встроенные функции error_log с дополнительной структурой данных, включающей:
| Элемент логирования | Описание |
|---|---|
| IP-адрес | Позволяет выявлять источники повторяющихся атак и блокировать их на уровне веб-сервера. |
| Время запроса | Фиксирует точный момент попытки внедрения для анализа временных закономерностей атак. |
| HTTP метод и URI | Определяет тип запроса (GET, POST) и конкретный эндпоинт, на который направлена атака. |
| Входные параметры | Сохраняет данные полей формы или URL-параметров, вызвавших срабатывание фильтров. |
| Тип обнаруженной угрозы | Классифицирует попытку по шаблонам: UNION, OR 1=1, подстановки с комментариями и др. |
Для мониторинга полезно настроить уведомления при превышении порога подозрительных запросов. Например, при более чем 10 попытках инъекций с одного IP за час следует автоматически отправлять уведомление на администраторский e-mail или в систему SIEM.
Важно хранить логи в отдельной базе данных или файле с ограниченным доступом, предотвращая их модификацию со стороны атакующего. Использование хэширования и цифровой подписи файлов логов повышает их целостность.
Регулярный анализ логов позволяет выявлять новые паттерны атак и обновлять фильтры SQL запросов, предотвращая эксплуатацию ранее неизвестных уязвимостей.
Вопрос-ответ:
Что такое SQL-инъекция и почему она опасна для PHP-приложений?
SQL-инъекция — это способ внедрения вредоносных SQL-запросов через пользовательский ввод. Если приложение формирует запросы к базе данных напрямую с данными от пользователя, злоумышленник может изменять или получать информацию, к которой не должен иметь доступ, вплоть до полного удаления данных. В PHP это особенно актуально, поскольку популярные функции, такие как mysqli_query или старые mysql_query, без фильтрации ввода делают систему уязвимой.
Какие методы защиты от SQL-инъекций считаются надёжными в PHP?
Наиболее надёжным подходом считается использование подготовленных выражений (prepared statements) с привязкой параметров. Это можно реализовать через PDO или mysqli. Принцип работы заключается в том, что структура запроса и данные от пользователя обрабатываются отдельно, что исключает возможность внедрения вредоносного кода. Дополнительно стоит использовать строгую фильтрацию и валидацию данных, особенно если они будут использоваться в динамических частях запросов.
Можно ли полностью защитить сайт от SQL-инъекций только с помощью экранирования строк?
Экранирование строк (например, через mysqli_real_escape_string) снижает риск атак, но не даёт полной защиты. Оно уязвимо к ошибкам при неправильном использовании и не защищает от случаев, когда ввод применяется в числовых или логических выражениях. Подготовленные выражения обеспечивают более надёжную защиту, потому что параметры передаются базе отдельно от запроса и не интерпретируются как код.
Как проверять пользовательский ввод в PHP перед использованием в запросах?
Сначала необходимо определить, какие данные допустимы в конкретном поле. Для числовых значений можно применять приведение типов или функцию filter_var с фильтром FILTER_VALIDATE_INT. Для строк рекомендуется ограничивать длину и использовать регулярные выражения для проверки формата. После этого данные следует вставлять в запрос через подготовленные выражения, а не напрямую. Такой подход снижает вероятность успешной атаки и позволяет избежать ошибок на уровне базы данных.
Насколько безопасно использовать ORM в PHP вместо ручных SQL-запросов?
ORM (Object-Relational Mapping) автоматически формирует SQL-запросы на основе объектов и их свойств, что уменьшает вероятность прямых SQL-инъекций. Большинство популярных ORM для PHP, таких как Doctrine или Eloquent, используют внутренние механизмы привязки параметров. Однако это не освобождает от необходимости проверять и фильтровать пользовательский ввод, особенно если он влияет на динамическую генерацию условий или сортировку в запросах.
Какие способы защиты от SQL инъекций в PHP являются безопасными для начинающих?
Для защиты от SQL инъекций в PHP стоит использовать подготовленные выражения (prepared statements) с привязкой параметров. Они исключают возможность вставки вредоносного кода через данные, введённые пользователем, потому что SQL-команда и данные обрабатываются отдельно. В PHP это реализуется через расширения PDO или MySQLi. Например, с PDO запрос к базе может выглядеть так: $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email"); $stmt->execute(['email' => $email]);. Такой подход надёжнее простого экранирования строк и позволяет безопасно работать даже с динамическими данными.
