Валидация и фильтрация данных в PHP: рабочие приёмы
filter_var, htmlspecialchars и подготовленные запросы решают разные задачи, и путаница между ними даёт то дыры в безопасности, то потерянные данные. Разбираем, где чему место.

Форма пропускает email@ как валидный адрес, а поиск ломается на кавычке в запросе. Две классические ошибки растут из одного корня: разработчик путает валидацию (данные корректны?), санитизацию (убрать лишнее) и экранирование (безопасно вывести). Это три разные операции на трёх разных этапах, и подменять одну другой опасно. Разберём рабочие приёмы для PHP 8.x — что применять на входе, что перед записью в БД, что перед выводом.
Три этапа, которые нельзя смешивать
Данные проходят три контрольные точки, и на каждой своя задача:
- Валидация — проверка, что значение соответствует ожиданиям: это действительно число, email, дата в диапазоне. Невалидное — отклоняем с ошибкой, не пытаемся «починить».
- Санитизация — приведение к нужному виду: обрезать пробелы, убрать управляющие символы. Меняет данные.
- Экранирование — подготовка к безопасной передаче в другой контекст: HTML, SQL, shell. Зависит от места назначения.
Главная ошибка новичков — экранировать при вводе. Если прогнать данные через htmlspecialchars() перед записью в БД, в базе окажется & вместо &, и при выводе в JSON или в письмо всё поедет. Экранирование делается в момент вывода и под конкретный контекст.
Валидируйте на входе, экранируйте на выходе. Между ними данные хранятся в чистом, «сыром» виде — тогда одно и то же значение одинаково корректно уйдёт и в HTML, и в CSV, и в API.
Валидация: filter_var и его границы
Встроенный filter_var() закрывает типовые проверки без регулярок. Ключевой момент: используйте константы с префиксом FILTER_VALIDATE_, а не FILTER_SANITIZE_ — это разные вещи.
<?php
// PHP 8.x
$email = $_POST['email'] ?? '';
// Валидация: вернёт строку или false
$validEmail = filter_var($email, FILTER_VALIDATE_EMAIL);
if ($validEmail === false) {
// адрес некорректен — отклоняем
http_response_code(422);
exit('Неверный email');
}
// Число в диапазоне 1..100
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 100],
]);
if ($age === false) {
exit('Возраст вне допустимого диапазона');
}
Осторожно с нулём и false: FILTER_VALIDATE_INT для строки "0" вернёт int(0), а 0 == false в нестрогом сравнении. Поэтому проверяйте строго через === false, иначе валидный ноль улетит в ветку ошибки.
Ещё одна ловушка: FILTER_VALIDATE_EMAIL проверяет синтаксис по RFC, но не существование ящика. Он пропустит a@b без точки в домене как валидный в некоторых версиях. Если нужна проверка домена — это отдельная задача (DNS-запись MX), filter_var её не решает.
Булевы значения из форм
Чекбокс присылает "on", скрытое поле — "1" или "true". Приводить руками не нужно:
<?php
// FILTER_NULL_ON_FAILURE вернёт null для непонятных значений,
// иначе '0', 'false', '' дают false, а 'on', '1', 'true' — true
$agree = filter_var(
$_POST['agree'] ?? '',
FILTER_VALIDATE_BOOLEAN,
FILTER_NULL_ON_FAILURE
);
if ($agree !== true) {
exit('Нужно согласие с условиями');
}
Санитизация: обрезать, но не калечить
Начиная с PHP 8.1 фильтр FILTER_SANITIZE_STRING объявлен устаревшим и удалён в 8.x — не используйте его, он подчёркивал ложное чувство безопасности, вырезая теги, но не защищая от XSS. Для строк применяйте адресные функции:
<?php
$name = $_POST['name'] ?? '';
// Убрать пробелы по краям
$name = trim($name);
// Убрать управляющие символы, но оставить буквы любых алфавитов
$name = preg_replace('/[\x00-\x1F\x7F]/u', '', $name);
// Ограничить длину в символах (не байтах) для UTF-8
if (mb_strlen($name) > 100) {
$name = mb_substr($name, 0, 100);
}
Флаг u в регулярке обязателен для UTF-8: без него preg_replace обработает многобайтовые символы побайтно и порежет кириллицу пополам, оставив битые последовательности.
Экранирование: под каждый контекст своё
Одна из главных мыслей: нет универсального «безопасного» вида данных. Безопасность определяется тем, куда данные попадают.
| Куда выводим | Чем экранировать | От чего защищает |
|---|---|---|
| HTML-текст | htmlspecialchars() с ENT_QUOTES | XSS через теги и атрибуты |
| Значение SQL | подготовленные запросы (PDO/mysqli) | SQL-инъекции |
| URL-параметр | urlencode() / rawurlencode() | подмену параметров, поломку ссылки |
| Аргумент shell | escapeshellarg() | инъекции команд |
| JSON | json_encode() | поломку структуры ответа |
HTML: ENT_QUOTES и кодировка
<?php
// PHP 8.1+ по умолчанию использует UTF-8 и ENT_QUOTES | ENT_SUBSTITUTE,
// но указывать явно — надёжнее при переносе кода между версиями
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
Без ENT_QUOTES одинарные кавычки не экранируются, и если данные попадают в атрибут вида value='...', остаётся дыра для XSS. ENT_SUBSTITUTE заменяет битые UTF-8 последовательности на символ замены вместо возврата пустой строки — иначе одна кривая последовательность обнулит весь вывод.
SQL: только подготовленные запросы
Никаких mysqli_real_escape_string со склейкой строк — это путь к ошибкам. Параметризация разделяет код запроса и данные:
<?php
// PDO, PHP 8.x
$pdo = new PDO(
'mysql:host=localhost;dbname=shop;charset=utf8mb4',
'app_user',
'YOUR_DB_PASSWORD',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$stmt = $pdo->prepare(
'SELECT id, title FROM products WHERE category_id = :cat AND price < :max'
);
$stmt->execute([':cat' => $categoryId, ':max' => $maxPrice]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Важное ограничение: параметры подставляются только на место значений. Имя таблицы, имя столбца или направление сортировки (ASC/DESC) через плейсхолдер не передаются. Если сортировка приходит от пользователя — проверяйте её через белый список, а не подставляйте в запрос:
<?php
$allowedSort = ['price', 'title', 'created_at'];
$sort = in_array($_GET['sort'] ?? '', $allowedSort, true)
? $_GET['sort']
: 'created_at';
// $sort теперь безопасно вставить в SQL — он из фиксированного списка
Массивы и вложенные данные
Форма с полями items[] присылает массив, и filter_var на нём молча вернёт false вместо проверки каждого элемента. Для массивов есть filter_var_array и флаг FILTER_REQUIRE_ARRAY:
<?php
// Каждый элемент items[] должен быть целым числом
$ids = filter_var(
$_POST['items'] ?? [],
FILTER_VALIDATE_INT,
FILTER_REQUIRE_ARRAY
);
// невалидные элементы станут false — отфильтруем их
$ids = array_filter($ids, fn($v) => $v !== false);
Когда что выбирать
- Простые скалярные проверки (email, число, URL, IP) —
filter_var, без регулярок. - Сложные правила (пароль с требованиями, формат артикула) — регулярка с флагом
uили самописная логика. - Много полей со связями (обязательные, зависимые) — библиотека валидации (например, symfony/validator или respect/validation), встроенных средств не хватит.
- Любой вывод — экранирование под контекст, всегда в момент вывода, не при сохранении.
Что ломается на практике
- Нестрогое сравнение результата
filter_var. Валидный0или""проваливается в ветку ошибки. Сравнивайте через=== false. - Регулярка без флага
uрежет UTF-8 и оставляет битые байты, которые потом ломаютhtmlspecialchars. - Двойное экранирование: данные экранированы при вводе и ещё раз при выводе — пользователь видит
&quot;вместо кавычки. - Плейсхолдер на месте имени столбца — запрос падает или тихо игнорирует условие. Только белый список.
- Доверие к
Content-Typeи заголовкам при загрузке файлов — их подделывают. Проверяйте реальный тип черезfinfo, а не$_FILES['x']['type'].
Что настроить дальше
Валидация — только первый слой. После неё смотрите на защиту от CSRF (токен в формах), заголовок Content-Security-Policy как второй рубеж против XSS и лимиты на размер и частоту запросов. Загрузка файлов — отдельная большая тема: проверка MIME через finfo, хранение вне webroot, случайные имена. Если полей много и правила повторяются от формы к форме — выносите валидацию в отдельный слой или библиотеку, чтобы не дублировать проверки по контроллерам.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: PHP Manual — Filter Functions (filter_var), PHP Manual — PDO Prepared Statements
Частые вопросы
У меня PHP 7.4, а вы пишете про 8.x — приёмы подойдут?
filter_var с FILTER_VALIDATE_EMAIL пропустил странный адрес — это баг?
Не проще ли один раз всё экранировать при записи в базу и не думать?
Если не проверить входные данные — что мне грозит?
Почему нельзя подставить имя столбца через плейсхолдер PDO?
Стоит ли писать свою валидацию или взять библиотеку?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.