Подготовленные запросы PDO: 100% защита от SQL-инъекций?

Онлайн-тренажер Python для начинающих

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

Начать курс

Введение: вопрос, который мучает каждого PHP-разработчика

Если вы когда-нибудь писали SQL-запросы в PHP, то наверняка слышали мантру: "Используй PDO и prepared statements — и будешь в безопасности". Но так ли это на самом деле? На Stack Overflow вопрос "Are PDO prepared statements sufficient to prevent SQL injection?" собрал тысячи просмотров и десятки жарких обсуждений. И не зря.

В этой статье мы не просто дадим короткий ответ "да" или "нет". Мы залезем под капот PDO, разберем, как именно работают подготовленные запросы, и выясним, в каких случаях даже PDO может вас подвести. Готовы? Поехали.

Что такое SQL-инъекция и почему она страшна?

Прежде чем говорить о защите, давайте вспомним врага в лицо. SQL-инъекция — это атака, при которой злоумышленник "впрыскивает" вредоносный SQL-код через входные данные. Классика:

// Плохой код: никогда так не делайте
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $query);

Если злоумышленник передаст admin' OR '1'='1, запрос превратится в:

SELECT * FROM users WHERE username = 'admin' OR '1'='1'

Вуаля — доступ к данным без пароля. А если передать '; DROP TABLE users; --? Прощай, таблица пользователей.

PDO и подготовленные запросы: как это работает?

PDO (PHP Data Objects) предлагает механизм prepared statements (подготовленных выражений). Идея простая: вы отделяете структуру запроса от данных. Вместо того чтобы вставлять переменные прямо в строку, вы используете плейсхолдеры:

// Безопасный код с PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute([':username' => $_POST['username']]);

Но что происходит под капотом? Это ключевой момент, который многие упускают.

Режимы эмуляции vs. нативные prepared statements

PDO может работать в двух режимах:

  • Режим эмуляции (по умолчанию в старых версиях PHP) — PDO сам экранирует данные и подставляет их в запрос на стороне PHP. Это всё ещё безопасно, но теоретически уязвимо, если экранирование сломается (например, при работе с нестандартными кодировками).
  • Нативный режим — PDO отправляет на сервер БД два сообщения: сначала шаблон запроса, потом отдельно данные. Сервер сам обрабатывает данные как значения, а не как код. Это абсолютно безопасно.

Чтобы включить нативный режим, отключите эмуляцию:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Теперь запрос отправляется на сервер в два этапа. Даже если злоумышленник передаст ' OR '1'='1, сервер MySQL или PostgreSQL просто воспримет это как строковое значение, а не как часть SQL-кода. Магия!

Но всё ли так идеально? Подводные камни PDO

Вернёмся к вопросу со Stack Overflow. Ответ "да, PDO prepared statements достаточны" — но с оговорками. Вот ситуации, когда защита может дать сбой.

1. Динамические идентификаторы (имена таблиц, столбцов)

Prepared statements работают только для значений (данных). Если вы подставляете имя таблицы или столбца, плейсхолдеры не помогут:

// Опасно! Имя таблицы нельзя передавать через плейсхолдер
$stmt = $pdo->prepare("SELECT * FROM :table WHERE id = :id");
$stmt->execute([':table' => $_GET['table'], ':id' => 1]); // Ошибка!

PDO просто вставит имя таблицы как строку, и запрос сломается. Если же вы используете эмуляцию, злоумышленник может передать users; DROP TABLE users; --. Решение: белый список допустимых значений.

2. LIKE-запросы и символы подстановки

Если вы используете LIKE с пользовательским вводом, prepared statements защитят от инъекций, но не от неожиданного поведения:

$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE :pattern");
$stmt->execute([':pattern' => "%{$_GET['search']}%"]);

Всё безопасно. Но если пользователь введёт % или _, они будут интерпретироваться как подстановочные символы. Это не инъекция, но может привести к утечке данных или медленным запросам. Используйте addcslashes() для экранирования wildcard-символов.

3. Ошибки в bindParam vs. bindValue

Многие путают эти методы. bindValue передаёт значение, а bindParam — ссылку на переменную:

$value = 'safe';
$stmt->bindParam(':param', $value, PDO::PARAM_STR);
$value = 'malicious'; // Меняем значение после bindParam
$stmt->execute(); // Используется 'malicious'!

Это не уязвимость, но может привести к путанице. Всегда используйте bindValue или передавайте массив в execute(), если не уверены.

Реальный пример: как хакер может обойти PDO?

Сценарий: разработчик использует PDO, но оставляет эмуляцию включённой и работает с кодировкой GBK (китайский стандарт). В этой кодировке некоторые многобайтовые символы могут "съедать" экранирующий слеш. Пример:

// Установка кодировки (опасно!)
$pdo->exec("SET NAMES 'GBK'");
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$_GET['name']]);

Если злоумышленник передаёт %bf%27 OR 1=1 --, в GBK последовательность %bf%27 (где %27 — кавычка) может быть интерпретирована как один символ, и экранирование сломается. Решение: всегда используйте PDO::ATTR_EMULATE_PREPARES = false и устанавливайте кодировку через DSN:

$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);


                
                

Блоги

Рекомендаций по книгам