Avatar ·

Достаточно ли подготовленных запросов PDO для защиты от SQL-инъекций?

Всем привет. Я начинающий PHP-разработчик, и у меня возник серьезный вопрос по безопасности. Я читал много статей, где говорят, что использование PDO с подготовленными запросами (prepared statements) полностью решает проблему SQL-инъекций. Но, честно говоря, я немного запутался.

Например, я пишу такой код:

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

Вроде бы все безопасно. Но я видел предупреждения, что PDO не всегда защищает, особенно если неправильно использовать. Например, что будет, если я вставлю данные в запрос с помощью query() или если я использую bindParam() с неправильным типом? А что насчет случаев, когда я динамически строю запрос, например, добавляю сортировку или LIMIT? Есть ли подводные камни?

Может быть, есть ситуации, когда PDO-подготовленные запросы все равно пропускают инъекцию? Или достаточно всегда использовать плейсхолдеры, и я могу спать спокойно? Буду благодарен за развернутый ответ с примерами, когда PDO может подвести.

<<>>

Короткий ответ: да, но с важными оговорками

Подготовленные запросы PDO (и mysqli) действительно защищают от SQL-инъекций, если вы используете их правильно. Однако есть несколько ситуаций, когда защита ломается. Давайте разберем все по порядку.

Как работает защита PDO

Когда вы используете плейсхолдеры (:email или ?), PDO отправляет на сервер MySQL два отдельных пакета:

  1. Шаблон запроса (без данных) — SELECT * FROM users WHERE email = ?
  2. Данные, которые сервер подставляет как чистые строки, а не как часть SQL-кода

Сервер знает: всё, что пришло в пакете с данными — это всегда значение, даже если там написано ' OR 1=1 --. Это и есть причина безопасности.

Когда PDO НЕ защищает (реальные подводные камни)

  • Динамические части запроса (имена таблиц, столбцов, ORDER BY, LIMIT)
    // ОПАСНО! Плейсхолдеры нельзя использовать для идентификаторов
    $stmt = $pdo->prepare("SELECT * FROM users ORDER BY {$_GET['sort']}");
    Решение: белый список разрешенных значений.
  • Использование query() или exec() с пользовательскими данными
    // ОПАСНО! Нет подготовки
    $pdo->query("SELECT * FROM users WHERE email = '{$_POST['email']}'");
    Решение: всегда используйте prepare() + execute().
  • Эмуляция подготовленных запросов (PDO::ATTR_EMULATE_PREPARES)
    По умолчанию PDO может эмулировать подготовку на клиенте, а не на сервере. В редких случаях (старые версии MySQL, неправильные кодировки) это может привести к инъекции. Решение: отключайте эмуляцию:
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  • Использование bindParam() с неправильным типом
    $stmt->bindParam(':id', $id, PDO::PARAM_INT); // если $id — строка, все равно безопасно
    Указание типа (PARAM_INT) не обязательно для защиты от инъекций, но может помочь с производительностью.
  • LIKE-запросы с подстановкой
    // Безопасно, но % нужно добавлять в PHP
    $stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE :name");
    $stmt->execute([':name' => '%' . $_GET['q'] . '%']);
    Плейсхолдер работает, сам % не является инъекцией.

Лучшая практика

<?php
// 1. Всегда отключаем эмуляцию
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// 2. Для данных — только плейсхолдеры
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
$stmt->execute([':name' => $_POST['name'], ':email' => $_POST['email']]);

// 3. Для динамических частей — белый список
$allowedSort = ['name', 'email', 'id'];
$sort = in_array($_GET['sort'], $allowedSort) ? $_GET['sort'] : 'id';
$stmt = $pdo->prepare("SELECT * FROM users ORDER BY $sort"); // безопасно, т.к. значение из белого списка
?>

Итог

Подготовленные запросы PDO — достаточная защита от SQL-инъекций для всех значений (строк, чисел, дат). Но вы должны:

  • Не смешивать данные с SQL-кодом вручную
  • Не использовать плейсхолдеры для идентификаторов (таблиц, столбцов)
  • Отключить эмуляцию подготовленных запросов на старых версиях MySQL
  • Проверять все динамические части через белый список

Если соблюдать эти правила — да, PDO полностью защищает.

<<>>

Дополнение: безопасность на уровне базы данных

Коллега выше все верно написал. Хочу добавить, что даже с идеальным PDO есть еще один слой защиты, который стоит использовать — это экранирование на уровне SQL-сервера.

Например, в MySQL есть режим NO_BACKSLASH_ESCAPES и настройки sql_mode. Если вы отключили эмуляцию PDO (ATTR_EMULATE_PREPARES = false), то сервер сам обрабатывает данные как бинарные значения, и никакая экранировка не нужна. Но если эмуляция включена (по умолчанию в некоторых конфигурациях), PDO экранирует данные через mysql_real_escape_string() — это тоже безопасно, но зависит от кодировки соединения.

Совет: всегда устанавливайте кодировку соединения явно:

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

И последнее: если вы используете raw SQL (например, для миграций или сложных запросов), никогда не подставляйте данные напрямую. Даже если кажется, что "это же просто число". Используйте quote() как запасной вариант:

$safeValue = $pdo->quote($_GET['value']); // экранирует и оборачивает в кавычки
$pdo->exec("UPDATE users SET name = $safeValue"); // безопасно, но лучше prepare

Но prepare + execute — это золотой стандарт, от которого не стоит отходить.