PHP: Очистка переменных в PHP: валидация, нормализация и экранирование

В PHP нет универсальной функции, которая делает любое пользовательское значение безопасным. Термин «очистка переменной» объединяет несколько разных операций: получение данных, нормализацию, проверку формата, проверку бизнес-правил и экранирование для конкретного места вывода.

Рабочая схема выглядит так:

  1. Проверить, что входное значение имеет ожидаемый тип.
  2. Нормализовать данные, например удалить пробелы по краям.
  3. Проверить формат, длину, диапазон и бизнес-ограничения.
  4. Передавать значения в SQL через параметры подготовленного запроса.
  5. Экранировать данные непосредственно перед выводом в 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 применять проверенный санитайзер с белым списком.

Документация

up: Просмотров: 3.5k
Материал помог? Оценка помогает находить устаревшие и спорные решения.

Комментарии

0

Нашли ошибку или хотите дополнить материал?

Комментариев пока нет.Можно первым дополнить материал или сообщить об ошибке.
Telegram