В PHP нет универсальной функции, которая делает любое пользовательское значение безопасным. Термин «очистка переменной» объединяет несколько разных операций: получение данных, нормализацию, проверку формата, проверку бизнес-правил и экранирование для конкретного места вывода.
Рабочая схема выглядит так:
- Проверить, что входное значение имеет ожидаемый тип.
- Нормализовать данные, например удалить пробелы по краям.
- Проверить формат, длину, диапазон и бизнес-ограничения.
- Передавать значения в SQL через параметры подготовленного запроса.
- Экранировать данные непосредственно перед выводом в HTML, JavaScript или другой контекст.
Когда этот материал не подходит: загрузка файлов, обработка вложенных JSON-структур и публикация разрешённого пользователю HTML требуют отдельных правил и специализированных инструментов.
Что делают основные функции PHP
trim()удаляет пробельные символы с начала и конца строки. Это нормализация, а не защита.filter_var()проверяет email, URL, число, IP-адрес и другие форматы.htmlspecialchars()подготавливает строку для вывода в HTML.strip_tags()удаляет HTML- и PHP-теги, но не является защитой от XSS.PDO::prepare()и параметры запроса отделяют SQL-код от пользовательских значений.
Готовое решение: обработка формы в PHP 8
Пример получает имя, email, возраст и комментарий. Значения проверяются до записи в базу, а секреты подключения читаются из переменных окружения.
<?php
declare(strict_types=1);
function readPostString(string $key): ?string
{
if (!array_key_exists($key, $_POST))
{
return null;
}
$value = $_POST[$key];
// Защита от name[]=value и других неожиданных массивов.
if (!is_string($value))
{
return null;
}
$value = trim($value);
if (!mb_check_encoding($value, 'UTF-8'))
{
return null;
}
return $value;
}
$name = readPostString('name');
$email = readPostString('email');
$ageRaw = readPostString('age');
$comment = readPostString('comment');
$errors = [];
if ($name === null || $name === '')
{
$errors['name'] = 'Укажите имя';
}
elseif (mb_strlen($name, 'UTF-8') > 80)
{
$errors['name'] = 'Имя не должно быть длиннее 80 символов';
}
$validatedEmail = $email !== null
? filter_var($email, FILTER_VALIDATE_EMAIL)
: false;
if ($validatedEmail === false)
{
$errors['email'] = 'Укажите корректный email';
}
else
{
$email = $validatedEmail;
}
$age = $ageRaw !== null
? filter_var(
$ageRaw,
FILTER_VALIDATE_INT,
[
'options' => [
'min_range' => 18,
'max_range' => 120,
],
]
)
: false;
if ($age === false)
{
$errors['age'] = 'Возраст должен быть целым числом от 18 до 120';
}
if ($comment === null)
{
$comment = '';
}
elseif (mb_strlen($comment, 'UTF-8') > 2000)
{
$errors['comment'] = 'Комментарий не должен быть длиннее 2000 символов';
}
if ($errors !== [])
{
http_response_code(422);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(
[
'ok' => false,
'errors' => $errors,
],
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
exit;
}
$dsn = (string)getenv('DB_DSN');
$dbUser = (string)getenv('DB_USER');
$dbPassword = (string)getenv('DB_PASSWORD');
if ($dsn === '' || $dbUser === '' || $dbPassword === '')
{
throw new RuntimeException('Database environment variables are not configured');
}
$pdo = new PDO(
$dsn,
$dbUser,
$dbPassword,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
$stmt = $pdo->prepare(
'INSERT INTO Feedback (Name, Email, Age, Comment)
VALUES (:name, :email, :age, :comment)'
);
$stmt->execute([
':name' => $name,
':email' => $email,
':age' => $age,
':comment' => $comment,
]);
http_response_code(201);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(
[
'ok' => true,
'id' => (int)$pdo->lastInsertId(),
],
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
Экранирование при выводе в HTML
Проверенные данные всё равно нужно экранировать при выводе. Пользователь может законно ввести символы <, >, кавычки и амперсанд.
function escapeHtml(string $value): string
{
return htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5,
'UTF-8'
);
}
Пример вывода в текст HTML и атрибут формы:
<p><?= escapeHtml($comment) ?></p> <input type="text" name="name" value="<?= escapeHtml($name) ?>" >
Лучше хранить в базе исходное нормализованное значение и экранировать его при каждом выводе. Если сохранить уже преобразованные HTML-сущности, данные окажутся привязаны к одному формату и могут пройти двойное экранирование.
Проверка email
$email = trim($emailRaw);
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false)
{
throw new InvalidArgumentException('Некорректный email');
}
FILTER_VALIDATE_EMAIL проверяет синтаксис адреса. Существование ящика подтверждается письмом со ссылкой или одноразовым кодом.
Проверка целого числа и диапазона
$quantity = filter_var(
$quantityRaw,
FILTER_VALIDATE_INT,
[
'options' => [
'min_range' => 1,
'max_range' => 100,
],
]
);
if ($quantity === false)
{
throw new InvalidArgumentException('Количество должно быть от 1 до 100');
}
Результат нужно сравнивать через === false. Значение 0 может быть допустимым числом и при нестрогой проверке ошибочно воспринимается как невалидное.
Проверка HTTP- и HTTPS-адреса
FILTER_VALIDATE_URL проверяет структуру URL, но обычно приложению также требуется список разрешённых схем.
function validateHttpUrl(string $value): ?string
{
$url = filter_var(trim($value), FILTER_VALIDATE_URL);
if ($url === false)
{
return null;
}
$scheme = strtolower((string)parse_url($url, PHP_URL_SCHEME));
if (!in_array($scheme, ['http', 'https'], true))
{
return null;
}
return $url;
}
Перед вставкой проверенного URL в HTML-атрибут его всё равно нужно пропустить через escapeHtml().
Проверка по разрешённому формату
Регулярное выражение лучше использовать для проверки, а не для молчаливого удаления неподходящих символов. Иначе разные исходные значения могут неожиданно превратиться в одну строку.
$promoCode = strtoupper(trim($promoCodeRaw));
if (preg_match('/\A[A-Z0-9-]{4,32}\z/', $promoCode) !== 1)
{
throw new InvalidArgumentException('Некорректный промокод');
}
Почему strip_tags() не защищает от XSS
strip_tags() подходит, когда из текста действительно нужно удалить разметку. Использовать её как универсальный XSS-фильтр нельзя.
$plainText = strip_tags($html);
Особенно опасен список разрешённых тегов:
// Тег останется вместе с потенциально опасными атрибутами. $result = strip_tags($html, ['a', 'p']);
strip_tags() не проверяет атрибуты разрешённых тегов. Если пользователю разрешено публиковать HTML, нужен HTML-санитайзер с белым списком тегов, атрибутов и URL-протоколов.
Почему FILTER_SANITIZE_STRING больше не нужен
FILTER_SANITIZE_STRING и его псевдоним FILTER_SANITIZE_STRIPPED устарели начиная с PHP 8.1. Их поведение смешивало удаление тегов и преобразование отдельных символов, но не обеспечивало полноценную защиту для конкретного контекста.
// Устаревший вариант — не использовать. $clean = filter_var($value, FILTER_SANITIZE_STRING);
Выбирайте операцию по задаче:
- проверить формат —
FILTER_VALIDATE_*; - вывести обычный текст в HTML —
htmlspecialchars(); - записать значение в SQL — подготовленный запрос;
- разрешить часть HTML — специализированный HTML-санитайзер;
- получить строку без разметки —
strip_tags(), но не как защиту от XSS.
SQL-инъекции: экранирование строки не заменяет параметры
Для значений из формы используйте подготовленные запросы. Не собирайте SQL конкатенацией и не применяйте addslashes() как защиту базы данных.
$stmt = $pdo->prepare( 'SELECT id, Email FROM Users WHERE Email = :email LIMIT 1' ); $stmt->execute([ ':email' => $email, ]); $user = $stmt->fetch();
Плейсхолдеры предназначены для значений. Названия таблиц, столбцов и направление сортировки нельзя безопасно передать обычным параметром — их выбирают из заранее заданного списка.
Вывод данных в JavaScript
Не вставляйте строку между кавычками вручную. Передайте значение через json_encode():
<script> const userData = <?= json_encode( $userData, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR ) ?>; </script>
Краевые случаи
- Поле формы может прийти массивом:
name[]=value. Передtrim()проверяйтеis_string(). - Пустая строка, отсутствующее поле и строка
"0"— разные значения. filter_var()без явного фильтра используетFILTER_DEFAULT, который фактически не фильтрует значение.- Ограничивайте длину до записи в базу, логирования и сложной обработки.
- Для UTF-8 считайте символы через
mb_strlen(), а не байты черезstrlen(). - Не удаляйте символы из имени, адреса или комментария без явного требования. Это может повредить легальные данные.
- Проверка email не доказывает, что ящик существует и принадлежит пользователю.
- URL может быть синтаксически корректным, но вести во внутреннюю сеть. При серверной загрузке URL отдельно защищайтесь от SSRF.
Безопасность
- Проверяйте данные на сервере, даже если форма содержит HTML-атрибуты
required,minилиpattern. - Для изменяющих состояние запросов добавляйте CSRF-токен.
- Не храните пароли базы данных в исходном коде или публичном репозитории.
- Не возвращайте пользователю текст исключений PDO и другие внутренние детали.
- Пароли пользователей хешируйте через
password_hash(), а не очищайте как обычные строки. - Загрузка файлов требует проверки MIME-типа, размера, расширения и безопасного каталога хранения.
Производительность
Проверка типа, длины и простых форматов почти не влияет на скорость обычной формы. Лимит длины полезно проверять до регулярных выражений, внешних API, записи в базу и логирования. Экранируйте значение при выводе, а не многократно на каждом промежуточном этапе.
Совместимость
Примеры рассчитаны на PHP 8.1+ и подходят для PHP 8.2, 8.3, 8.4 и более новых версий ветки PHP 8. Для mb_check_encoding() и mb_strlen() требуется расширение mbstring, для работы с MySQL через PDO — pdo_mysql.
Как проверить результат
Сначала проверьте синтаксис файла:
php -l clearvar.php
Запустите встроенный сервер PHP:
php -S 127.0.0.1:8080
Корректный запрос:
curl -i -X POST http://127.0.0.1:8080/clearvar.php \ --data-urlencode 'name=Александр' \ --data-urlencode 'email=alex@example.com' \ --data-urlencode 'age=38' \ --data-urlencode 'comment=<script>alert(1)</script>'
Ожидается HTTP-код 201. Строка со <script> может храниться как обычный текст, но при выводе через escapeHtml() должна отображаться текстом и не выполняться браузером.
Проверка некорректного типа поля:
curl -i -X POST http://127.0.0.1:8080/clearvar.php \ --data 'name[]=admin&email=alex@example.com&age=38'
Ожидается HTTP-код 422 и ошибка для поля name.
Альтернативы
- В крупном проекте вынести правила в DTO, отдельные валидаторы или слой обработки команд.
- Использовать валидатор фреймворка для вложенных структур, локализованных ошибок и повторного применения правил.
- Для API описывать входные данные схемой и отклонять неизвестные поля.
- Для разрешённого HTML применять проверенный санитайзер с белым списком.
Комментарии
0Нашли ошибку или хотите дополнить материал?