PDO в PHP: спасает ли он от SQL-инъекций на 100%?

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

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

Начать курс

Введение: Мифы и реальность PDO

Если вы пишете на PHP и хоть раз гуглили «как защититься от SQL-инъекций», то наверняка слышали мантру: «Используйте PDO и prepared statements — и будет вам счастье». Это золотой стандарт, основа любого учебника по PHP-безопасности. Но так ли это на самом деле? Достаточно ли просто написать $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?') и забыть о проблеме?

Вопрос «Are PDO prepared statements sufficient to prevent SQL injection?» набрал тысячи просмотров на Stack Overflow не просто так. Ответ, как выясняется, не такой однозначный. В этой статье мы залезем под капот PDO, разберем, как на самом деле работают подготовленные запросы, и выясним, в каких случаях даже PDO может вас подвести.

Это не скучная теория. Мы рассмотрим реальные примеры, эмуляцию запросов, нюансы с кодировками и дадим конкретные action items, чтобы ваш код был по-настоящему защищен.

Как работают Prepared Statements: Магия или математика?

Для начала давайте вспомним, что такое SQL-инъекция. Это когда злоумышленник передает в запрос данные, которые интерпретируются как часть SQL-кода. Классика: ' OR 1=1 --.

Prepared statements (подготовленные запросы) решают эту проблему кардинально. Они разделяют логику запроса и данные. Выглядит это так:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([':email' => $_GET['email'], ':status' => 'active']);
?>

Под капотом происходит следующее:

  1. Подготовка (Prepare): Драйвер отправляет серверу БД «скелет» запроса: SELECT * FROM users WHERE email = ? AND status = ?. Сервер парсит этот скелет, оптимизирует его и запоминает план выполнения. Место для данных (плейсхолдеры) пока пусто.
  2. Выполнение (Execute): Теперь вы отправляете только фактические значения. Сервер вставляет их в уже готовый план, но делает это особым образом — он знает, что это данные, а не код. Поэтому любые попытки «закрыть кавычку» или добавить SQL-команду просто превращаются в часть строкового литерала.

Звучит идеально. Но есть нюанс.

Подводный камень: Эмуляция Prepared Statements

Главный сюрприз, который выявил тот самый вопрос на Stack Overflow: PDO по умолчанию не всегда использует «настоящие» prepared statements.

Дело в том, что не все базы данных поддерживают эту фичу на уровне протокола. А некоторые драйверы (например, для SQLite или старых версий MySQL) работают через эмуляцию. Что это значит?

Когда эмуляция включена, PDO сам подставляет значения в SQL-запрос на стороне клиента (в PHP), а затем отправляет уже готовый запрос серверу. При этом он экранирует спецсимволы с помощью PDO::quote().

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

Пример: Атака через разные кодировки

Классическая атака с использованием неверной кодировки выглядит так. Предположим, вы установили соединение с MySQL в кодировке GBK (китайская кодировка), а сам запрос эмулируется. Символ \ (обратный слеш) в GBK может быть частью многобайтового символа.

Злоумышленник передает строку: %BF%27. В кодировке GBK последовательность %BF%5C образует один символ, а %27 — это просто кавычка. Эмулятор видит кавычку и экранирует ее, превращая %27 в %5C%27 (то есть \'). Но из-за того, что %BF «съедает» добавленный обратный слеш, получается %BF%5C%27, где %BF%5C — это один символ, а %27 — это неэкранированная кавычка. Инъекция удалась!

Это не баг PDO, это баг конфигурации. Но он очень распространен и опасен.

Как проверить, что использует ваш PDO?

Давайте напишем простой скрипт, который покажет, что происходит под капотом вашего PDO-соединения:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');

// Проверяем, включена ли эмуляция
$emulate = $pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES);
echo 'Эмуляция включена: ' . ($emulate ? 'Да' : 'Нет') . "\n";

// Смотрим версию драйвера
echo 'Версия драйвера: ' . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . ' ' .
     $pdo->getAttribute(PDO::ATTR_SERVER_VERSION) . "\n";

// Отключаем эмуляцию (если драйвер поддерживает реальные prepared statements)
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
echo 'Эмуляция отключена. Теперь используем настоящие prepared statements.' . "\n";
?>

Если вы используете MySQL 5.1+ и драйвер mysqlnd (родной драйвер PHP, который идет в комплекте с PHP 5.3+), то вы можете и должны отключить эмуляцию. Это заставит PDO отправлять на сервер настоящие подготовленные запросы, и вся магия экранирования будет происходить на стороне БД, а не в PHP.

Правильная настройка: Чек-лист безопасности

Итак, чтобы ответить на вопрос «Достаточно ли PDO?» — да, достаточно, но только при правильной настройке. Вот что нужно сделать обязательно:

  • Всегда указывайте кодировку в DSN. Никогда не полагайтесь на настройки по умолчанию. Пишите charset=utf8mb4 (или utf8, если вам не нужны эмодзи). Это самый важный пункт.
  • Отключайте эмуляцию. $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);. Это заставит драйвер использовать реаль

Блоги

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