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 username = :username"); $stmt->execute([':username' => $_GET['username']]); $user = $stmt->fetch(); ``` Я думаю, это безопасно, так как я использую плейсхолдеры. Но потом я увидел такой код в старом проекте: ```php $pdo->query("SELECT * FROM users WHERE id = " . $_GET['id']); ``` И мне стало интересно: а что если я случайно забуду использовать подготовленные запросы в каком-то месте? Или если я использую PDO, но неправильно настраиваю эмуляцию подготовленных запросов? Может ли злоумышленник обойти защиту PDO? Я также слышал про динамические запросы (например, ORDER BY или LIMIT), где плейсхолдеры не работают. Как быть в таких случаях? Нужно ли мне дополнительно экранировать данные или есть другие подводные камни? Объясните, пожалуйста, насколько PDO prepared statements действительно защищают, и какие есть нюансы, о которых новички вроде меня могут не знать. Стоит ли полностью полагаться на PDO или нужно комбинировать с другими методами защиты? <<>>

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

PDO подготовленные запросы (prepared statements) — это основной и наиболее надёжный способ защиты от SQL-инъекций, но они не являются панацеей. В 95% случаев их достаточно, но есть важные нюансы, которые нужно понимать.

Как работают подготовленные запросы

Когда вы используете PDO с плейсхолдерами (:param или ?), запрос и данные отправляются на сервер БД раздельно. Сервер сначала компилирует шаблон запроса, а потом подставляет данные, которые всегда трактуются как данные, а не как SQL-код. Это делает инъекцию невозможной в рамках одного запроса.

// Безопасно: данные и запрос разделены
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => "user@example.com'; DROP TABLE users;--"]);
// Результат: поиск пользователя с email, содержащим спецсимволы, но не выполнение DROP

Когда подготовленные запросы НЕ защищают

  • Динамические части SQL (ORDER BY, LIMIT, имена таблиц/столбцов). Плейсхолдеры работают только для значений, не для идентификаторов или ключевых слов SQL.
  • Эмуляция подготовленных запросов (PDO::ATTR_EMULATE_PREPARES). По умолчанию в некоторых конфигурациях PDO может эмулировать подготовку на стороне PHP, а не на сервере БД. Это может привести к уязвимостям, если эмуляция настроена неправильно.
  • Использование сырых запросов с конкатенацией. Если вы хотя бы раз используете $pdo->query("SELECT ... WHERE id = " . $id) — вы теряете всю защиту.

Как правильно настроить PDO

Вот минимальная безопасная конфигурация:

// Отключаем эмуляцию — используем настоящие подготовленные запросы
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass', [
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::MYSQL_ATTR_DIRECT_QUERY => false, // для MySQL
]);

// Теперь все prepare() будут отправлять запрос на сервер БД
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);

Что делать с ORDER BY, LIMIT, именами таблиц

Для этих случаев используйте белые списки (whitelisting):

$allowedColumns = ['id', 'name', 'email', 'created_at'];
$orderBy = $_GET['order'] ?? 'id';
if (!in_array($orderBy, $allowedColumns, true)) {
    $orderBy = 'id'; // Значение по умолчанию
}

$allowedDirections = ['ASC', 'DESC'];
$direction = strtoupper($_GET['dir'] ?? 'ASC');
if (!in_array($direction, $allowedDirections, true)) {
    $direction = 'ASC';
}

$stmt = $pdo->prepare("SELECT * FROM users ORDER BY $orderBy $direction");
$stmt->execute();

Дополнительные меры безопасности

  • Никогда не доверяйте пользовательскому вводу — даже если вы используете PDO, всегда проверяйте типы данных (int, float, bool).
  • Используйте строгие типы: $stmt->bindValue(':id', (int)$_GET['id'], PDO::PARAM_INT).
  • Не используйте устаревшие функции вроде mysql_real_escape_string() — они не дают полной защиты.
  • Регулярно обновляйте PHP и PDO — новые версии исправляют потенциальные уязвимости.

Итог

PDO подготовленные запросы с отключённой эмуляцией — это 99% защиты от SQL-инъекций. Оставшийся 1% — это случаи с динамическими частями SQL, где нужны белые списки. Комбинируйте PDO с валидацией входных данных, и ваше приложение будет в безопасности.

<<>>

Альтернативный взгляд: не только PDO, но и архитектура

Хотя PDO подготовленные запросы — отличный инструмент, я рекомендую смотреть шире. Даже с идеальным PDO вы можете допустить ошибку, если в одном месте забудете использовать плейсхолдер. Поэтому добавьте многоуровневую защиту:

1. Используйте ORM (Doctrine, Eloquent)

ORM автоматически экранирует данные и редко позволяет писать сырые SQL-запросы. Это снижает риск человеческой ошибки:

// Eloquent ORM
$user = User::where('email', $request->input('email'))->first();

2. Валидация и фильтрация на входе

Проверяйте типы данных до того, как они попадут в запрос:

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
    throw new InvalidArgumentException('Invalid ID');
}

3. Регулярный аудит кода

Используйте статические анализаторы (PHPStan, Psalm) и автоматические проверки безопасности. Они найдут места, где вы случайно используете конкатенацию строк в запросах.

PDO — это замок на двери, но не забывайте про сигнализацию и камеры. Комбинируйте подготовленные запросы с валидацией, ORM и автоматическими проверками — тогда SQL-инъекции вам не страшны.