SQL‑инъекции — распространённая проблема: если вставлять пользовательские данные прямо в запрос, злоумышленник может исказить логику и получить доступ к чужим данным или испортить базу.
PDO (PHP Data Objects) позволяет защититься с помощью подготовленных выражений. Принцип простой: сначала задаёте шаблон запроса с параметрами (теми самыми плейсхолдерами), а потом отдельно передаёте туда значения. База данных воспринимает эти значения строго как данные, а не как часть SQL‑кода.
Как это выглядит на практике
SELECT с подготовленным выражением
Допустим, нужно получить пользователя по email. Небезопасный вариант выглядел бы так:
$email = $_GET['email']; $sql = "SELECT * FROM users WHERE email = '$email'";
Если в email передать ' OR '1'='1, запрос вернёт всех пользователей.
С подготовленным выражением в PDO делают так:
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $email = $_GET['email'] ?? ''; $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email'); $stmt->execute([':email' => $email]); $user = $stmt->fetch(PDO::FETCH_ASSOC);
Здесь :email — параметр. PDO сам корректно передаёт значение, исключая риск инъекции.
INSERT с подготовленным выражением
Для вставки данных логика та же: сначала шаблон, потом данные. Пример добавления пользователя:
$name = $_POST['name'] ?? ''; $email = $_POST['email'] ?? ''; $stmt = $pdo->prepare( 'INSERT INTO users (name, email, created_at) VALUES (:name, :email, NOW())' ); $stmt->execute([ ':name' => $name, ':email' => $email, ]);
Такой INSERT не позволит внедрить вредоносный SQL через поля формы.
Какие параметры бывают
В PDO используют два основных стиля:
- Именованные параметры:
:name,:email. Удобно, когда параметров много — сразу понятно, что куда идёт. - Позиционные параметры:
?. Значения передаются в том же порядке, в каком стоят знаки вопроса.
Пример с позиционными параметрами:
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)'); $stmt->execute([$name, $email]);
Именованные обычно читаются лучше и реже приводят к ошибкам при изменении запроса.
Частые ошибки, из‑за которых защита не работает
- Склеивание запроса с пользовательскими данными. Даже если дальше используется
prepare, риск остаётся, если часть запроса вы собираете вручную из ввода пользователя. - Попытка подставить имена таблиц или колонок через параметр. В PDO параметры работают только для значений. Имена таблиц и колонок нужно проверять по белому списку допустимых значений и вставлять явно:
php
$allowedTables = ['users', 'posts']; $table = in_array($_GET['table'], $allowedTables, true) ? $_GET['table'] : 'users'; $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE id = :id"); $stmt->execute([':id' => $_GET['id']]);
- Отключение обработки ошибок или игнорирование настроек безопасности. Не меняйте настройки PDO, отвечающие за обработку ошибок и экранирование, без чёткого понимания последствий.
Полезные приёмы
- Получить сразу все строки можно через
fetchAll:php$stmt = $pdo->prepare('SELECT id, name FROM users WHERE active = :active'); $stmt->execute([':active' => 1]); $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
- Проверить, сколько строк затронуто, помогает
rowCount()— это удобно после UPDATE, DELETE или INSERT:php$stmt = $pdo->prepare('UPDATE users SET name = :name WHERE id = :id'); $stmt->execute([':name' => $newName, ':id' => $id]); $affected = $stmt->rowCount();
- Всегда указывайте кодировку соединения (
charset=utf8mb4в DSN) и включайте режим исключений (ERRMODE_EXCEPTION), чтобы ошибки не оставались незамеченными. - Подготовленные выражения защищают от SQL‑инъекций, но не закрывают другие риски: нужна ещё валидация данных, проверка прав доступа, ограничения на уровне базы и т. д.
- Один подготовленный запрос можно выполнять многократно с разными значениями — это и удобно, и эффективнее по производительности.
