Обработка формы и защита от инъекций в PHP: короткий гайд
Как принять данные из формы и не открыть дыру для SQL-инъекций и XSS. Подготовленные выражения PDO, экранирование вывода и проверка входа за один заход, без лишней теории.

Форма обратной связи на PHP пишется за десять минут, а потом живёт годами и собирает боты, спам и попытки инъекций. Здесь разберём минимальный, но безопасный путь: принять данные, проверить их, записать в базу через подготовленное выражение и вывести обратно так, чтобы <script> из поля «имя» не выполнился в браузере. Займёт примерно полчаса, если PHP и MySQL уже стоят.
Материал написан под PHP 8.1+ и работу с базой через PDO. Разбор внутренностей парсера SQL и настройки WAF — отдельная тема, здесь только прикладной минимум для обычной формы.
Что нужно до начала
- PHP 8.1 или новее (проверьте командой
php -v); - расширение
pdo_mysql— включено по умолчанию в большинстве сборок, проверяется черезphp -m | grep pdo; - база данных MySQL или MariaDB и таблица, куда писать;
- доступ к файлам сайта и возможность запустить PHP-скрипт (локальный сервер или хостинг).
Для примера будет таблица messages с полями id, name, email, body, created_at.
Две угрозы, которые лечатся по-разному
Новички часто путают SQL-инъекцию и XSS и пытаются закрыть обе одной функцией. Это разные уязвимости на разных этапах.
- SQL-инъекция — когда пользовательский текст попадает в SQL-запрос и меняет его логику. Лечится на этапе запроса к базе подготовленными выражениями.
- XSS1 — когда пользовательский текст попадает в HTML-страницу и выполняется как код в чужом браузере. Лечится на этапе вывода экранированием.
Отсюда главное правило: данные экранируют не при получении, а при использовании — и по-разному в зависимости от того, куда они идут.
Не существует «универсальной очистки» переменной один раз при приёме. Одна и та же строка безопасна для базы через плейсхолдер и опасна в HTML без htmlspecialchars.
Шаг 1. Приём и первичная проверка данных
Из $_POST данные приходят в виде строк. Не доверяйте им, но и не калечьте функциями вроде устаревшего mysql_real_escape_string — она давно удалена. Сначала проверяем, что поля есть и не пустые.
<?php
// PHP 8.1+, форма отправлена методом POST
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Метод не поддерживается');
}
// Забираем значения с запасным пустым значением
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$body = trim($_POST['body'] ?? '');
$errors = [];
if ($name === '' || mb_strlen($name) > 100) {
$errors[] = 'Имя обязательно и не длиннее 100 символов';
}
// filter_var для email — стандартный способ проверки формата
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Некорректный email';
}
if ($body === '' || mb_strlen($body) > 2000) {
$errors[] = 'Сообщение обязательно и не длиннее 2000 символов';
}
if ($errors) {
http_response_code(422);
// Здесь выводим ошибки пользователю (экранирование — на шаге 3)
exit(implode("\n", $errors));
}
Проверка: отправьте форму с пустым именем — должно вернуться сообщение об ошибке и код 422, запись в базу не должна создаться.
Обратите внимание: filter_var с FILTER_VALIDATE_EMAIL проверяет только формат адреса, а не то, что почта существует. Это нормально для формы — доставку письма проверит уже отправитель.
Шаг 2. Запись в базу через подготовленное выражение
Ключевой момент защиты от SQL-инъекций — не подставлять переменные в текст запроса, а передавать их отдельно через плейсхолдеры. PDO делает это встроенно.
<?php
// Подключение к базе, PDO с исключениями вместо тихих ошибок
$dsn = 'mysql:host=localhost;dbname=mydb;charset=utf8mb4';
try {
$pdo = new PDO($dsn, 'db_user', 'YOUR_DB_PASSWORD', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// Отключаем эмуляцию — используются настоящие подготовленные выражения
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
http_response_code(500);
// Текст ошибки не показываем пользователю, только логируем
error_log($e->getMessage());
exit('Ошибка подключения');
}
// Именованные плейсхолдеры :name, :email, :body
$sql = 'INSERT INTO messages (name, email, body, created_at)
VALUES (:name, :email, :body, NOW())';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':name' => $name,
':email' => $email,
':body' => $body,
]);
$newId = (int) $pdo->lastInsertId();
Проверка: отправьте в поле имени строку Robert'); DROP TABLE messages;--. Запись должна создаться с этим текстом как обычным именем, а таблица остаться на месте. Это и есть работающая защита.
Почему так: при ATTR_EMULATE_PREPARES => false драйвер отправляет в MySQL сам запрос и значения раздельно. База не воспринимает переданный текст как часть SQL, поэтому кавычки и точки с запятой внутри имени безвредны.
Указывайте charset=utf8mb4 в DSN. Без него в некоторых конфигурациях возможны проблемы с кодировкой, а эмуляция подготовленных выражений совместно с неверной кодировкой исторически была вектором инъекций.
Шаг 3. Безопасный вывод обратно на страницу
Данные из базы или из формы нельзя вставлять в HTML напрямую. Иначе строка <script>alert(1)</script>, сохранённая злоумышленником, выполнится у каждого, кто откроет страницу.
<?php
// Читаем последние сообщения
$rows = $pdo->query('SELECT name, email, body FROM messages
ORDER BY id DESC LIMIT 20')->fetchAll();
function esc(string $value): string {
// ENT_QUOTES экранирует и двойные, и одинарные кавычки
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
?>
<ul>
<?php foreach ($rows as $row): ?>
<li>
<strong><?= esc($row['name']) ?></strong>
(<?= esc($row['email']) ?>):
<?= nl2br(esc($row['body'])) ?>
</li>
<?php endforeach; ?>
</ul>
Проверка: сохраните через форму сообщение с текстом <b>test</b> и откройте список. На странице должно быть видно буквально <b>test</b>, а не жирное слово. Значит тег экранирован и не исполнился.
Порядок важен: сначала htmlspecialchars, потом nl2br. Если сделать наоборот, экранирование сломает вставленные теги <br>.
Если не получилось
- Fatal error: Uncaught PDOException ... could not find driver — не включено расширение
pdo_mysql. Проверьтеphp -mи раскомментируйте строку расширения вphp.ini. - SQLSTATE[HY000] [1045] Access denied for user — неверный логин или пароль базы в конструкторе PDO. Сверьте данные подключения.
- SQLSTATE[42S02] Base table or view not found — нет таблицы
messagesили указана не та база в DSN. - Вместо русских букв «крякозябры» — не совпадает кодировка. Проверьте, что в DSN стоит
charset=utf8mb4, а таблица создана в кодировкеutf8mb4. - Теги из формы отображаются как жирный текст — забыли
htmlspecialcharsпри выводе. Оберните каждое пользовательское значение вesc().
Что настроить дальше
Базовая форма готова. Следующие шаги, которые стоит добавить перед выкладкой в прод:
- CSRF-токен — скрытое поле с одноразовым значением из сессии, чтобы форму нельзя было отправить с чужого сайта. Это отдельная тема, но без неё форма уязвима к подделке запросов.
- Ограничение частоты — простой лимит по IP или капча, иначе форму быстро найдут спам-боты.
- Логирование ошибок в файл вместо вывода пользователю: технические сообщения PDO не должны попадать на страницу.
Точные сигнатуры функций и доступные флаги filter_var и htmlspecialchars для вашей версии PHP сверяйте в официальной документации — набор фильтров и значения по умолчанию менялись между версиями.
1 XSS (Cross-Site Scripting) — межсайтовый скриптинг: внедрение в страницу чужого JavaScript, который выполняется в браузере других посетителей и может красть куки или подменять содержимое.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: PHP Manual — PDO Prepared Statements, PHP Manual — htmlspecialchars
Частые вопросы
У меня старый PHP 7.4, этот код заработает?
Достаточно ли htmlspecialchars, чтобы полностью защититься от XSS?
Нужно ли всё равно чистить данные, если я использую подготовленные выражения?
А если я сохранил данные без экранирования, я в опасности?
Можно ли вставлять переменные прямо в строку запроса, если экранировать кавычки вручную?
Почему в примере отключена эмуляция подготовленных выражений?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.