Обработка HTML-форм в PHP: валидация, CSRF и безопасный вывод

PHP / Безопасность

Обработка HTML-форм в PHP: валидация, CSRF и безопасный вывод

Как читать GET и POST в PHP, проверять строки, массивы и allowlist, защищать форму CSRF и показывать ошибки без XSS.

Опубликовано 04.09.2026 6 минут чтения
Содержание статьи

Данные формы недоверенные, даже если HTML ограничивает длину и варианты. Пользователь может изменить разметку или отправить HTTP-запрос вручную. PHP-обработчик должен проверить метод, структуру каждого значения, допустимый диапазон, CSRF-токен и права.

Форма профиля

HTML
<form action="/profile" method="post">  <input type="hidden" name="csrf_token" value="...">​  <label for="display-name">Отображаемое имя</label>  <input id="display-name" name="display_name" type="text" maxlength="80" required>​  <label for="email">Email</label>  <input id="email" name="email" type="email" maxlength="254" required>​  <fieldset>    <legend>Темы рассылки</legend>    <label><input type="checkbox" name="topics[]" value="html"> HTML</label>    <label><input type="checkbox" name="topics[]" value="css"> CSS</label>    <label><input type="checkbox" name="topics[]" value="php"> PHP</label>  </fieldset>​  <button type="submit">Сохранить</button></form>​

Квадратные скобки в topics[] создают массив в $_POST. Если ни один флажок не выбран, ключ может отсутствовать.

Проверка метода

PHP
<?php​declare(strict_types=1);​if ($_SERVER['REQUEST_METHOD'] !== 'POST') {    header('Allow: POST');    http_response_code(405);    exit('Метод не поддерживается.');}​

Не определяйте отправку только через isset($_POST['submit']): кнопка может не передаться, а имя кнопки не является HTTP-контрактом.

Получение строк

Значение с ожидаемым ключом может оказаться массивом. Сначала проверьте тип:

PHP
$displayName = is_string($_POST['display_name'] ?? null)    ? trim($_POST['display_name'])    : '';​$email = is_string($_POST['email'] ?? null)    ? trim($_POST['email'])    : '';​

Вызов trim($_POST['display_name']) без проверки способен вызвать TypeError. Не используйте (string) для массива: преобразование не делает ввод корректным.

Валидация

PHP
$errors = [];​if ($displayName === '') {    $errors['display_name'] = 'Введите отображаемое имя.';} elseif (mb_strlen($displayName) > 80) {    $errors['display_name'] = 'Имя должно быть не длиннее 80 символов.';}​if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {    $errors['email'] = 'Введите корректный email.';}​

filter_var() проверяет формат email, но не существование ящика и не владение им. Для подписки используйте double opt-in.

Не удаляйте из строки все «неподходящие» символы, превращая один ввод в другой. Для большинства полей лучше отклонить значение и объяснить правило.

Массив и allowlist

PHP
$allowedTopics = ['html', 'css', 'php'];$topicsInput = $_POST['topics'] ?? [];$topics = [];​if (!is_array($topicsInput)) {    $errors['topics'] = 'Некорректный формат списка тем.';} else {    foreach ($topicsInput as $topic) {        if (!is_string($topic) || !in_array($topic, $allowedTopics, true)) {            $errors['topics'] = 'Выбрана неизвестная тема.';            break;        }​        $topics[] = $topic;    }​    $topics = array_values(array_unique($topics));}​

Сервер знает допустимые варианты и не доверяет значениям checkbox. Строгое сравнение в in_array(..., true) не включает неявное преобразование типов.

Числа

PHP
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [    'options' => [        'min_range' => 14,        'max_range' => 120,    ],]);​if (!is_int($age)) {    $errors['age'] = 'Возраст должен быть целым числом от 14 до 120.';}​

Не проверяйте число только через is_numeric(): эта функция допускает форматы, которые могут не соответствовать бизнес-правилу. После разбора всегда проверяйте диапазон.

CSRF-защита

До вывода формы создайте сессию и токен:

PHP
session_start();​if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));}​

В форме:

PHP
<input  type="hidden"  name="csrf_token"  value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">​

В обработчике до изменения данных:

PHP
$token = is_string($_POST['csrf_token'] ?? null)    ? $_POST['csrf_token']    : '';​if (!hash_equals($_SESSION['csrf_token'], $token)) {    http_response_code(403);    exit('Не удалось проверить запрос. Обновите страницу.');}​

CSRF-токен не заменяет проверку авторизации. Сервер должен отдельно убедиться, что текущий пользователь вправе менять именно этот объект.

Безопасный вывод значений

PHP
<input  id="display-name"  name="display_name"  type="text"  value="<?= htmlspecialchars($displayName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">​

Для текстового контекста HTML используйте htmlspecialchars(). Не храните данные «навсегда экранированными»: для SQL, JSON, URL и JavaScript существуют другие контексты.

Ошибка также выводится безопасно:

PHP
<?php if (isset($errors['display_name'])): ?>  <p id="display-name-error">    <?= htmlspecialchars($errors['display_name'], ENT_QUOTES, 'UTF-8') ?>  </p><?php endif; ?>​

Свяжите поле с ошибкой через aria-describedby и добавьте aria-invalid="true".

Сохранение

PHP
if ($errors === []) {    $statement = $database->prepare(        'UPDATE users SET display_name = :display_name, email = :email WHERE id = :id'    );    $statement->execute([        'display_name' => $displayName,        'email' => $email,        'id' => $currentUserId,    ]);​    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));​    header('Location: /profile?saved=1', true, 303);    exit;}​

ID берётся из проверенной серверной сессии, а не из скрытого поля. SQL использует параметры. Redirect после успеха предотвращает повтор POST при обновлении.

GET-параметры

GET используют для поиска и фильтрации, которые не изменяют данные:

PHP
$query = is_string($_GET['q'] ?? null) ? trim($_GET['q']) : '';​if (mb_strlen($query) > 100) {    http_response_code(422);    exit('Поисковый запрос слишком длинный.');}​

Параметр из URL так же недоверен, как POST.

Частые ошибки

  • клиентские required и maxlength считают защитой сервера;
  • $_POST читают без проверки типа;
  • неизвестные варианты select и checkbox принимают без allowlist;
  • POST изменяет данные без CSRF и проверки прав;
  • значения вставляют в SQL строковой конкатенацией;
  • пользовательский текст выводят без контекстного экранирования;
  • подробное исключение базы показывают в браузере;
  • после успеха возвращают HTML напрямую и получают повторную отправку.

Что запомнить

Внешний ввод проходит последовательность: проверка структуры, нормализация, валидация, авторизация, безопасная запись и экранирование при выводе. HTML улучшает интерфейс, но PHP остаётся границей доверия.

Сообщить об ошибке в статье

Рекомендации редакции

Другие публикации из этого раздела.

Новые материалы

Получайте практические разборы без информационного шума

Одна общая рассылка о новых статьях и инструментах. Адрес будет активироваться только после подтверждения.

Подписка появится после подключения сервиса рассылок и Yandex SmartCaptcha.