Что именно приходит через GET и POST
В PHP $_GET и $_POST — это не «безопасные данные формы», а уже распарсенный внешний ввод. $_GET берётся из query string: /search?q=php&page=2. $_POST обычно заполняется для HTML-форм с application/x-www-form-urlencoded и multipart/form-data. Если клиент отправил JSON, $_POST чаще всего будет пустым: тело читают через php://input и затем разбирают через json_decode(), что ближе к теме JSON, сериализация и форматы данных.
Важно не путать источник данных с HTTP-методом. У POST-запроса может быть query string, а GET-запрос не должен менять состояние приложения. Для действий вроде «удалить», «оплатить», «сменить email» нужна отдельная защита, связанная с CSRF и state-changing запросы.
flowchart TD
A[HTTP-запрос] --> B{Где лежат данные?}
B --> C[Query string<br/>$_GET / INPUT_GET]
B --> D[HTML form body<br/>$_POST / INPUT_POST]
B --> E[JSON body<br/>php://input + json_decode]
C --> F[Проверить наличие<br/>missing vs empty]
D --> F
E --> F
F --> G[Валидация формы значения<br/>тип, диапазон, allowlist]
G --> H[Преобразование в нормальные типы<br/>int, string, DTO]
H --> I[Бизнес-логика]
I --> J[Отдельная защита на выходе<br/>escaping, prepared statements, CSRF]Валидация, sanitization и экранирование
Валидация отвечает на вопрос: «соответствует ли значение правилам?». Например, page должен быть целым числом от 1, sort — одним из разрешённых значений, email — синтаксически похожим на email.
Sanitization меняет значение: удаляет, кодирует или преобразует символы. Это полезно в отдельных случаях, но опасно как универсальная привычка. Если пользователь ввёл bo gus@example.com, FILTER_SANITIZE_EMAIL может убрать пробел и получить другую строку; это не значит, что пользователь действительно ввёл корректный адрес. Обычно правильнее: сначала trim(), потом валидировать, а при ошибке показать понятное сообщение.
Отдельно стоит экранирование вывода. Оно не заменяет валидацию. Значение может быть валидным именем пользователя и всё равно требовать htmlspecialchars() при выводе в HTML. Для SQL нужны prepared statements, а не ручная «очистка» строки; это уже тема Prepared statements и SQL injection. Для HTML-контекста — XSS, экранирование вывода и шаблоны.
filter_input: полезный, но не магический API
filter_input() читает внешнюю переменную из источника вроде INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER или INPUT_ENV и применяет фильтр. По умолчанию используется FILTER_DEFAULT, который является алиасом FILTER_UNSAFE_RAW, то есть фактически не фильтрует данные. Поэтому фильтр лучше указывать явно.
Типичный пример для страницы поиска:
<?php
declare(strict_types=1);
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1],
]);
if ($page === false || $page === null) {
$page = 1;
}
$sortRaw = filter_input(INPUT_GET, 'sort', FILTER_UNSAFE_RAW);
$allowedSorts = ['relevance', 'date', 'title'];
$sort = in_array($sortRaw, $allowedSorts, true) ? $sortRaw : 'relevance';
$q = filter_input(INPUT_GET, 'q', FILTER_UNSAFE_RAW);
$query = is_string($q) ? trim($q) : '';
if ($query === '') {
// Пустой поиск можно обработать отдельно: показать форму, подсказки или 400.
}Здесь page проходит числовую валидацию, sort проверяется через allowlist, а q остаётся обычной строкой, потому что «любая поисковая строка» не становится безопасной от случайного sanitizer. При выводе $query в HTML всё равно нужен htmlspecialchars().
Есть тонкость с возвращаемыми значениями: filter_input() возвращает значение при успехе, false при провале фильтра и null, если переменная не установлена. С флагом FILTER_NULL_ON_FAILURE поведение для failure становится null, а отсутствие переменной — false. Это удобно не всегда, поэтому в прикладном коде часто пишут маленькие функции-обёртки, чтобы не размазывать эти проверки по контроллерам.
Ещё одна деталь: filter_input() фильтрует исходные данные, полученные от SAPI, а не то, что вы позже руками записали в $_GET или $_POST. Если вы уже собрали массив сами, используйте filter_var() или обычную явную проверку.
Missing, empty и значение "0"
В PHP легко перепутать «поля нет», «поле есть, но пустое» и «поле равно нулю». Это особенно заметно в формах.
<?php
// /items?category=&page=0
var_dump(array_key_exists('category', $_GET)); // true
var_dump($_GET['category'] === ''); // true
var_dump(array_key_exists('page', $_GET)); // true
var_dump(empty($_GET['page'])); // true: строка "0" считается emptyempty() удобен для быстрых проверок, но для request data он часто слишком грубый. Строка "0" может быть валидным значением: номером страницы в API, ID, флагом, количеством. Поэтому лучше явно проверять наличие через array_key_exists() и отдельно валидировать значение.
Для формы профиля это выглядит так:
<?php
declare(strict_types=1);
$errors = [];
if (!array_key_exists('display_name', $_POST)) {
$errors['display_name'] = 'Поле обязательно.';
} elseif (!is_string($_POST['display_name'])) {
$errors['display_name'] = 'Некорректный формат.';
} else {
$displayName = trim($_POST['display_name']);
if ($displayName === '') {
$errors['display_name'] = 'Имя не должно быть пустым.';
} elseif (mb_strlen($displayName) > 80) {
$errors['display_name'] = 'Имя слишком длинное.';
}
}Здесь код отдельно различает отсутствующее поле, нестроковое значение и пустую строку. Нестрочное значение возможно, если клиент отправит display_name[]=x: PHP превратит такой параметр в массив. Для скалярных полей это надо отбрасывать, а не молча приводить к строке Array.
Массивы в query string и form data
HTML-формы и query string могут передавать массивы: tags[]=php&tags[]=http. Это нормальная часть PHP, но она требует явного контракта. Если поле должно быть строкой, проверяйте is_string(). Если поле должно быть массивом, проверяйте каждый элемент.
<?php
$tagsRaw = $_GET['tags'] ?? [];
if (!is_array($tagsRaw)) {
$tagsRaw = [$tagsRaw];
}
$allowedTags = ['php', 'http', 'security', 'database'];
$tags = [];
foreach ($tagsRaw as $tag) {
if (!is_string($tag)) {
continue;
}
$tag = trim($tag);
if (in_array($tag, $allowedTags, true)) {
$tags[] = $tag;
}
}
$tags = array_values(array_unique($tags));Для таких полей allowlist обычно надёжнее regex. Regex нужен для структурированных форматов, но его стоит писать так, чтобы он покрывал всю строку, а не искал «подходящий кусок» внутри мусора.
Безопасное преобразование user input
Хорошая схема короткая: прочитать внешний ввод на границе, проверить наличие, проверить форму, проверить бизнес-смысл, привести к нужному типу и передать дальше уже нормальные значения. Не сервис с $_POST внутри, а явный объект или массив.
<?php
declare(strict_types=1);
final class SearchInput
{
public function __construct(
public readonly string $query,
public readonly int $page,
public readonly string $sort,
) {}
}
function searchInputFromGet(array $get): SearchInput
{
$query = isset($get['q']) && is_string($get['q']) ? trim($get['q']) : '';
$page = filter_var($get['page'] ?? null, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1],
]);
$page = is_int($page) ? $page : 1;
$sortRaw = isset($get['sort']) && is_string($get['sort']) ? $get['sort'] : 'relevance';
$sort = in_array($sortRaw, ['relevance', 'date', 'title'], true)
? $sortRaw
: 'relevance';
return new SearchInput($query, $page, $sort);
}Такой код легко тестировать без реального HTTP-запроса и без изменения суперглобалов. Он хорошо сочетается с подходом из SAPI и суперглобалы: суперглобалы остаются на краю приложения, а внутрь передаются явные данные. В проектах с middleware эту роль часто берёт ServerRequestInterface из PSR-7, middleware и HTTP-клиенты.
Практическая карта
Для $_GET обычно валидируют параметры навигации и фильтрации: page, q, sort, tag, from, to. Для $_POST — данные формы, которые часто меняют состояние и поэтому требуют не только валидации, но и CSRF-защиты. Для JSON body — отдельное чтение php://input, проверка ошибки json_decode() и затем такая же валидация структуры.
Не пытайтесь «санитизировать всё на входе» одним вызовом. На входе нужно понять, что пользователь имел в виду и допустимо ли это значение. На выходе — экранировать под конкретный контекст. В базе — использовать параметры запросов. В HTTP-ответе — корректные статус-коды и заголовки, о чём отдельно говорит HTTP-заголовки, ответы и редиректы.
См. также
- SAPI и суперглобалы — откуда PHP берёт
$_GET,$_POSTи другие суперглобалы. - CSRF и state-changing запросы — почему POST-форма с валидными полями всё равно нуждается в защите от подделки запроса.
- XSS, экранирование вывода и шаблоны — почему output escaping не заменяется input filtering.
- Prepared statements и SQL injection — как безопасно передавать значения в SQL.
- JSON, сериализация и форматы данных — как работать с JSON body вместо обычной HTML-формы.
- PSR-7, middleware и HTTP-клиенты — объектная модель request data без прямой зависимости от суперглобалов.