GET и POST в PHP: как принять данные формы и защитить обработчик

GET и POST в PHP: как принять данные формы и защитить обработчик Полезное

GET и POST — это методы HTTP-запроса. При GET данные формы уходят в адресе страницы (строка запроса после ?), при POST — в теле запроса. В PHP их читают из суперглобальных массивов: $_GET содержит параметры строки запроса, $_POST — поля тела формы.

Разберем, как браузер собирает запрос, как прочитать значения в PHP без предупреждений, когда выбирать GET, а когда POST, и как довести обработчик формы до базовой защиты. Примеры проверены на PHP 8.4 (поддерживаемая ветка, самая новая на сентябрь 2026 — 8.5) и работают на PHP 8.1+.

Четыре термина, которые путают

Термин Что это Пример
Метод запроса Глагол HTTP, который выбирает браузер по атрибуту method формы GET, POST
Строка запроса (query string) Часть URL после ?, пары имя=значение через & ?q=php+8&page=2
Тело запроса Данные после заголовков HTTP-запроса name=Anna&message=Hello+PHP
Суперглобальный массив Массив PHP, который интерпретатор заполняет до запуска скрипта $_GET, $_POST, $_SERVER

Важная деталь: название $_GET говорит об источнике данных, а не о методе. $_GET заполняется из строки запроса при любом методе, поэтому POST на адрес feedback.php?from=footer даст и $_POST, и $_GET['from']. Сам метод смотрят в $_SERVER['REQUEST_METHOD'].

Минимальный пример с GET: форма поиска

Создайте файл search.php, запустите в той же папке встроенный сервер командой php -S localhost:8000 и откройте http://localhost:8000/search.php.

<?php
declare(strict_types=1);

// Берем только строку: ?q[]=x пришлет массив, а не строку
$q = isset($_GET['q']) && is_string($_GET['q']) ? trim($_GET['q']) : '';
?>
<!doctype html>
<meta charset="utf-8">
<form action="search.php" method="get">
  <input name="q" value="<?= htmlspecialchars($q) ?>">
  <button>Найти</button>
</form>
<?php if ($q !== ''): ?>
  <p>Результаты по запросу: <?= htmlspecialchars($q) ?></p>
<?php endif; ?>

Введите php 8 и нажмите кнопку. Адрес станет search.php?q=php+8, а под формой появится «Результаты по запросу: php 8». Пробел закодирован как +, PHP раскодирует его обратно при заполнении $_GET.

Что здесь происходит по шагам:

  1. Браузер берет адрес из action. Если action не указан, форма отправляется на текущий URL.
  2. Для каждого поля с атрибутом name собирает пару имя=значение. Поле без name не отправляется вообще.
  3. Кодирует пары (URL-encoding) и при method="get" дописывает их к адресу после ?.
  4. PHP разбирает строку запроса и кладет значения в $_GET — всегда строками или массивами строк.

htmlspecialchars превращает <, >, & и кавычки в HTML-сущности. Без него запрос ?q=<script>...</script> выполнился бы на странице — это отраженный XSS.

Как выглядит запрос на самом деле

Открыть сырой запрос можно в браузере: F12 -> вкладка Network (Сеть) -> клик по запросу -> Headers и Payload. GET из примера выше:

GET /search.php?q=php+8 HTTP/1.1
Host: localhost:8000

Та же форма обратной связи методом POST:

POST /feedback.php HTTP/1.1
Host: localhost:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

name=Anna&message=Hello+PHP

Данные те же пары имя=значение, но они в теле, а адрес не меняется. Content-Type говорит серверу, как разобрать тело. PHP заполняет $_POST только для application/x-www-form-urlencoded и multipart/form-data (формы с файлами).

GET или POST: как выбрать

Главный критерий — меняет ли запрос что-то на сервере. Чтение (поиск, фильтр, пагинация) — GET. Создание, изменение, удаление, вход, отправка файлов — POST.

Свойство GET POST
Где данные В URL В теле запроса
Видны в истории, закладках, логах сервера Да Обычно нет: стандартный access log пишет только адрес, но тело могут журналировать приложение, прокси, WAF и отладочные инструменты
Можно поделиться ссылкой на результат Да Нет
Кеширование браузером и прокси Возможно Как правило, нет
Повтор при обновлении страницы (F5) Молча Браузер спрашивает подтверждение
Объем данных Ограничен длиной URL на стороне сервера и клиента Ограничен настройками сервера и PHP (post_max_size)
Файлы Нельзя Можно, с enctype="multipart/form-data"

Две оговорки. Первая: POST не шифрует данные, он только убирает их из адреса. Пароль в теле POST по HTTP читается так же легко, как в URL, — защищает только HTTPS. И сам метод не гарантирует, что тело не попадет в журналы: чувствительные поля (пароли, токены, персональные данные) маскируйте в логах приложения и инфраструктуры. Вторая: GET по спецификации HTTP считается безопасным методом, то есть не должен менять состояние. Ссылка вида delete.php?id=5 опасна: ее может открыть предзагрузчик браузера или краулер.

Чтение значений без предупреждений

Частая ошибка новичка — обращаться к ключу, которого может не быть.

<?php
$page = $_GET['page']; // открыли list.php без ?page=
echo $page;

Фактический результат в PHP 8: Warning: Undefined array key "page" in .../list.php on line 2, а $page равна null. Исправление — проверить наличие и тип одновременно через filter_input:

<?php
declare(strict_types=1);

// null - параметра нет, false - параметр есть, но это не целое >= 1
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1],
]);

if ($page === false) {
    http_response_code(400);
    exit('Некорректный номер страницы');
}
$page ??= 1;

echo "Страница $page";

list.php выводит «Страница 1», list.php?page=3 — «Страница 3», list.php?page=abc и list.php?page=0 — ответ 400. Для простого значения по умолчанию хватает $_GET['sort'] ?? 'new', но помните: все, что пришло из запроса, — строки, и '3' не то же самое, что 3 при строгом сравнении.

Суперглобальный $_REQUEST объединяет GET, POST и (в зависимости от request_order и variables_order) cookie. В обработчиках форм его лучше не использовать: из кода не видно, откуда пришло значение.

Обработчик POST-формы с базовой защитой

Ниже учебный, но полный обработчик: форма обратной связи, которая проверяет CSRF-токен, валидирует поля, пишет в базу подготовленным запросом и перенаправляет после успеха. Для запуска нужны расширения pdo_sqlite и mbstring, в большинстве сборок PHP они есть.

Сначала структура папок. Файл базы нельзя класть рядом со скриптом: встроенный сервер (и обычный веб-сервер) раздает все файлы из корня сайта (document root), и feedback.sqlite с именами и сообщениями можно было бы скачать прямым запросом. Поэтому публичный скрипт лежит в public/, а база — уровнем выше, в var/:

project/
├── public/
│   └── feedback.php    <- корень сайта, доступен из браузера
└── var/
    └── feedback.sqlite  <- создастся при первой отправке, из браузера недоступен

Создайте папки командой mkdir -p public var, положите код ниже в public/feedback.php и запустите сервер из папки project с явным корнем: php -S localhost:8000 -t public.

<?php
// feedback.php - базовый защищенный обработчик POST-формы (учебный пример)
declare(strict_types=1);
session_start();

function e(string $s): string
{
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}

$errors = [];
$name = '';
$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf'] ?? '';
    if (!is_string($token) || !hash_equals($_SESSION['csrf'], $token)) {
        http_response_code(403);
        exit('Форма устарела, обновите страницу');
    }

    $name = is_string($_POST['name'] ?? null) ? trim($_POST['name']) : '';
    $message = is_string($_POST['message'] ?? null) ? trim($_POST['message']) : '';

    if ($name === '' || mb_strlen($name) > 100) {
        $errors[] = 'Имя: от 1 до 100 символов';
    }
    if ($message === '' || mb_strlen($message) > 2000) {
        $errors[] = 'Сообщение: от 1 до 2000 символов';
    }

    if (!$errors) {
        // База вне document root: public/../var/feedback.sqlite
        $pdo = new PDO('sqlite:' . dirname(__DIR__) . '/var/feedback.sqlite', null, null, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        ]);
        $pdo->exec('CREATE TABLE IF NOT EXISTS feedback (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            message TEXT NOT NULL
        )');
        $stmt = $pdo->prepare('INSERT INTO feedback (name, message) VALUES (:name, :message)');
        $stmt->execute([':name' => $name, ':message' => $message]);

        // Сообщение об успехе - в сессии на один показ, а не в GET-параметре:
        // ?sent=1 в адресе мог бы открыть кто угодно без отправки формы
        $_SESSION['flash'] = 'Спасибо, сообщение сохранено.';
        header('Location: feedback.php', true, 303); // Post/Redirect/Get
        exit;
    }
    http_response_code(422);
}
?>
<!doctype html>
<meta charset="utf-8">
<?php $flash = $_SESSION['flash'] ?? null; unset($_SESSION['flash']); ?>
<?php if (is_string($flash)): ?><p><?= e($flash) ?></p><?php endif; ?>
<?php foreach ($errors as $err): ?><p><?= e($err) ?></p><?php endforeach; ?>
<form action="feedback.php" method="post">
  <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
  <input name="name" value="<?= e($name) ?>">
  <textarea name="message"><?= e($message) ?></textarea>
  <button>Отправить</button>
</form>

Ожидаемое поведение: пустая форма вернет ответ 422 и строки с ошибками, введенное сохранится в полях. Заполненная форма запишет строку в var/feedback.sqlite и перенаправит на feedback.php с текстом «Спасибо, сообщение сохранено». Нажатие F5 после этого не отправит данные повторно и не покажет сообщение второй раз: оно живет в сессии ровно один показ.

Две проверки, которые стоит сделать руками:

  1. Запрос http://localhost:8000/feedback.sqlite должен вернуть 404 — файла базы в корне сайта нет. Если вместо ошибки начинается скачивание, база лежит в document root.
  2. Открытие http://localhost:8000/feedback.php в новой вкладке без отправки формы не должно показывать «Спасибо, сообщение сохранено».

От чего защищает каждый прием — и от чего нет:

Прием Закрывает угрозу Чего не делает
htmlspecialchars при выводе XSS: чужой HTML/JS в странице Не очищает данные для SQL или shell
Подготовленный запрос prepare + execute SQL-инъекцию через значения полей Не подставляет имена таблиц и колонок — их берут из белого списка
CSRF-токен в сессии + hash_equals Отправку формы с чужого сайта от имени пользователя Не заменяет авторизацию и проверку прав
Проверка типа и длины Массивы вместо строк, мусор и гигантские поля Не проверяет смысл данных (существует ли email)
Редирект 303 после POST Повторную отправку при обновлении страницы Не защищает от двойного клика — нужен уникальный ключ операции

Порядок важен, и это три разных действия: валидируем данные на входе; кодируем вывод под контекст, куда он попадает (htmlspecialchars для HTML, rawurlencode для части URL); значения для SQL не экранируем вручную, а передаем параметрами подготовленного запроса. Одна «универсальная очистка» всех данных на входе ломает текст и не закрывает все контексты.

Что еще нужно в production

Пример выше — базовый уровень, а не полностью защищенная форма. Перед боевым запуском добавьте:

  1. HTTPS на всем сайте и cookie сессии с флагами Secure, HttpOnly, SameSite=Lax.
  2. Ограничение частоты отправок (по IP или аккаунту) и капчу для публичных форм.
  3. Лимиты на уровне веб-сервера и PHP: размер тела (post_max_size) и число полей (max_input_vars).
  4. Журналирование ошибок в лог, а не на экран (display_errors=Off).
  5. Все, что не должно скачиваться из браузера, — вне document root: файлы SQLite, логи, конфиги с паролями, загруженные файлы. Для форм с файлами дополнительно проверяйте реальный MIME и размер.
  6. Журналирование без чувствительных данных: пароли, токены и персональные данные в логах маскируются.

Если данные приходят не через форму

Когда фронтенд отправляет JSON через fetch, $_POST будет пустым: PHP не разбирает application/json. Тело читают из потока php://input:

<?php
declare(strict_types=1);

try {
    $data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    http_response_code(400);
    exit('Некорректный JSON');
}
$name = is_array($data) && is_string($data['name'] ?? null) ? trim($data['name']) : '';

Дальше те же правила: проверка типов и длины, подготовленные запросы, экранирование при выводе. Для собственных запросов из PHP к чужим API обычно берут расширение cURL или HTTP-клиент вроде Guzzle.

Если не получилось

Симптом Вероятная причина Что сделать
$_POST пустой У формы нет method="post", у поля нет name или пришел JSON Проверить вкладку Payload в DevTools, для JSON читать php://input
$_POST и $_FILES пустые на больших данных Превышен post_max_size, PHP отбросил тело Смотреть лог ошибок PHP, поднять лимит или уменьшить данные
POST превращается в GET Сервер ответил редиректом 301/302 (например, добавил слеш в конце адреса) Указать в action точный конечный URL
could not find driver или unable to open database file Нет расширения pdo_sqlite или не создана папка var/ Проверить php -m, создать var/ с правами на запись
Undefined array key Обращение к отсутствующему ключу ??, isset или filter_input
Array to string conversion или TypeError: ... array given В запросе пришел name[]=..., а код ждет строку (echo, trim, htmlspecialchars) Проверять is_string перед обработкой
Кракозябры вместо кириллицы Страница не в UTF-8 <meta charset="utf-8"> и файлы в UTF-8

Дорожная карта для самостоятельной практики: соберите форму поиска на GET, затем форму обратной связи на POST из примера, откройте оба запроса в DevTools и попробуйте сломать обработчик: пустые поля, name[]=1, строка с <script>, отправка без токена. Результат — форма, которая на каждый такой ввод отвечает кодом 4xx, а не падает.

Выводы

  • GET передает данные в строке запроса URL, POST — в теле запроса; в PHP они попадают в $_GET и $_POST, причем $_GET заполняется при любом методе.
  • GET подходит для чтения и ссылок на результат (поиск, фильтры), POST — для действий, меняющих данные, входа и загрузки файлов.
  • POST не шифрует данные: конфиденциальность дает только HTTPS.
  • Значения из запроса — всегда строки или массивы: проверяйте наличие, тип и диапазон через ??, is_string, filter_input.
  • Базовая защита формы — экранирование при выводе, подготовленные запросы, CSRF-токен и редирект после POST; файлы базы и логов — вне document root. Для production этого мало.

Где применяется / связь с практикой

Прием данных из форм и API — ежедневная задача PHP-разработчика: регистрация, корзина, фильтры каталога, админки. В фреймворках вроде Laravel и Symfony те же $_GET и $_POST спрятаны за объектом запроса, валидаторами и встроенной CSRF-защитой, но понимать, что происходит под ними, все равно нужно. Следующий шаг после этой статьи — загрузка файлов в PHP, где POST-форма работает с multipart/form-data.

Освойте тему на практике

Системно пройти путь от обработки запросов до архитектуры приложений, баз данных и тестирования можно на курсе «PHP Developer». Разборы отдельных тем с преподавателями бывают на открытых уроках Otus.

FAQ

Можно ли отправить форму методом PUT или DELETE?
HTML-форма поддерживает только GET и POST (а также dialog). PUT и DELETE отправляют через fetch или используют скрытое поле _method, которое распознает фреймворк.

Есть ли точный лимит длины GET-запроса?
Спецификация HTTP жесткого лимита не задает, его определяют сервер, прокси и браузер. На практике адреса длиннее нескольких килобайт рискуют получить ошибку 414, поэтому большие данные отправляют POST.

Можно ли передать в одном запросе и GET-, и POST-параметры?
Да: POST на адрес со строкой запроса заполнит оба массива. Так часто передают служебный параметр в URL, а данные формы — в теле.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)