Вступление: почему SQL-инъекции всё ещё актуальны?
Представьте: ваш сайт работает как часы, пользователи регистрируются, оставляют комментарии, оформляют заказы. И вдруг — база данных оказывается в руках злоумышленника. Не потому, что хакеры гениальны, а потому, что кто-то забыл поставить знак вопроса в SQL-запросе.
SQL-инъекции — это не архаичная угроза из 2000-х. В 2024 году OWASP всё ещё ставит их в топ-3 самых опасных уязвимостей веб-приложений. А вопрос "How can I prevent SQL injection in PHP?" на Stack Overflow набрал более 1.5 миллиона просмотров — и продолжает расти. Значит, проблема жива.
В этой статье мы разберёмся, как намертво закрыть дверь для SQL-инъекций в PHP в 2025 году. Без магии, без сложных фреймворков, только чистый код и лучшие практики.
Что такое SQL-инъекция простыми словами?
SQL-инъекция — это когда пользовательский ввод становится частью SQL-команды, а не просто данными. Пример из жизни:
<?php
// ПЛОХОЙ КОД — никогда так не делайте!
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $query);
?>
Если злоумышленник введёт в поле username строку ' OR '1'='1, запрос превратится в:
SELECT * FROM users WHERE username = '' OR '1'='1'
Это вернёт всех пользователей. А если ввести '; DROP TABLE users; -- — можно потерять всю таблицу. Страшно? Ещё бы.
Лучшие практики защиты от SQL-инъекций в PHP 2025
1. Используйте PDO с подготовленными запросами (это стандарт)
PDO (PHP Data Objects) — это универсальный интерфейс для работы с базами данных. Его главное преимущество: он разделяет SQL-логику и данные. Вы пишете запрос с плейсхолдерами, а PDO сам заботится о безопасном экранировании.
<?php
// ХОРОШИЙ КОД — используйте PDO
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // отключаем эмуляцию
]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch();
?>
Почему это работает? База данных получает структуру запроса отдельно от данных. Даже если пользователь введёт вредоносный SQL, он будет интерпретирован как обычная строка, а не как часть команды.
2. Mysqli с подготовленными запросами (альтернатива)
Если вы работаете только с MySQL, можно использовать mysqli — но только с подготовленными запросами, а не с экранированием через mysqli_real_escape_string().
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'testdb');
$stmt = $mysqli->prepare('INSERT INTO comments (user_id, text) VALUES (?, ?)');
$stmt->bind_param('is', $userId, $commentText);
$stmt->execute();
?>
Важно: в 2025 году mysqli_real_escape_string() всё ещё существует, но полагаться на него для защиты — плохая идея. Он экранирует только строки, но не защищает от инъекций в числовых полях или в операторах LIKE.
3. Фильтрация и валидация — второй рубеж обороны
Подготовленные запросы — это основа. Но добавьте к ним фильтрацию по типу данных. Если ожидается число — проверьте, что это число. Если email — проверьте формат.
<?php
$id = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);
if ($id === false) {
throw new InvalidArgumentException('ID must be an integer');
}
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
?>
Комбинируйте filter_var(), ctype_digit() для чисел и регулярные выражения для сложных форматов. Это не только безопасность, но и корректность данных.
4. Избегайте динамических запросов любой ценой
Иногда так хочется собрать запрос из кусочков: добавить ORDER BY, изменить LIMIT, подставить имя таблицы. Это самый опасный путь. Если без динамики не обойтись, используйте белый список разрешённых значений:
<?php
$allowedColumns = ['id', 'name', 'email', 'created_at'];
$sortBy = $_GET['sort'] ?? 'id';
if (!in_array($sortBy, $allowedColumns, true)) {
$sortBy = 'id';
}
$stmt = $pdo->prepare("SELECT * FROM users ORDER BY $sortBy");
$stmt->execute();
?>
Никогда не подставляйте имена таблиц или столбцов напрямую из пользовательского ввода!
5. Настройте PDO правильно
Даже с подготовленными запросами можно оставить лазейку, если неправильно настроить PDO. Вот критически важные настройки:
- Отключите эмуляцию подготовленных запросов:
PDO::ATTR_EMULATE_PREPARES => false. Иначе PDO может подставлять данные в запрос на стороне PHP, а не на стороне БД, и вы теряете защиту. - Включите режим исключений:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION. Это поможет отлавливать ошибки раньше, чем они станут уязвимостью. - Установите кодировку в DSN:
charset=utf8mb4. Это защищает от некоторых атак через многобайтовые символы.
6. Используйте ORM для сложных проектов
Если вы строите крупное приложение, рассмотрите ORM (Object-Relational Mapping): Doctrine, Eloquent (Laravel), Propel. Они практически полностью исключают ручное написание SQL-запросов.
<?php
// Пример с Eloquent (Laravel)
$user = User::where('email', $request->input('email'))->first();
// Под капотом — подготовленные запросы
?>
ORM — это не