Накратко: Входа в PHP валидирате с filter_var() или filter_input_array() и ясни правила за всяко поле: тип, диапазон, дължина и разрешени стойности. Което не отговаря, се отхвърля, а не се преправя мълчаливо. Валидирането е важно за чисти данни и по-малка повърхност за атака, но не замества нито Prepared Statements срещу SQL инжекция, нито кодирането според контекста срещу XSS.
Защо „филтрирай всичко“ не е концепция
В много по-стари PHP проекти в началото на всеки файл има цикъл, който прекарва всички стойности от $_GET и $_POST през функция за почистване. Това изглежда сигурно. На практика понякога премахва твърде много, например апострофи във фамилни имена, а понякога твърде малко, защото една и съща функция трябва да важи едновременно за SQL, HTML и имена на файлове.
Смисленото валидиране работи по друг начин: то пита за всяко поле какво е разрешено там. Количеството в поръчката е цяло число между 1 и горна граница. Полето за сортиране е едно от три фиксирани стойности. Датата е в определен формат и след друга дата. Всичко останало се отхвърля.
OWASP разграничава две нива: синтактична проверка дали стойността има правилната форма и семантична проверка дали има смисъл в контекста. И двете принадлежат на сървъра; проверките в браузъра служат само за удобство.
Какви филтри носи PHP
Разширението Filter е включено в PHP по подразбиране. Най-важните филтри за валидиране според наръчника на PHP:
| Филтър | Проверява | Важни опции и бележки |
|---|---|---|
FILTER_VALIDATE_INT |
цяло число | min_range, max_range |
FILTER_VALIDATE_FLOAT |
число с десетични знаци | decimal за разделителния знак |
FILTER_VALIDATE_BOOL |
булева стойност | с FILTER_NULL_ON_FAILURE се различава от „невярно“ |
FILTER_VALIDATE_EMAIL |
формата на имейл адрес | не доказва, че пощенската кутия съществува |
FILTER_VALIDATE_URL |
формата на URL | проверете схемата сами, без интернационализирани домейни |
FILTER_VALIDATE_IP |
IP адрес | флагове за частни и запазени диапазони |
FILTER_VALIDATE_REGEXP |
собствен шаблон | ограничете шаблона с ^ и $ |
Филтърът за почистване FILTER_SANITIZE_STRING е остарял от PHP 8.1. Който още го ползва, трябва да премине валидирането към подходящи филтри, а за изхода да използва htmlspecialchars().
filter_var в PHP: как да проверите чисто един формуляр
С filter_input_array() цял формуляр може да се провери наведнъж спрямо дефиниция. Непознатите полета не се вземат:
<?php
declare(strict_types=1);
$regeln = [
'menge' => ['filter' => FILTER_VALIDATE_INT,
'options' => ['min_range' => 1, 'max_range' => 50]],
'email' => FILTER_VALIDATE_EMAIL,
'agb' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
'versand' => ['filter' => FILTER_VALIDATE_REGEXP,
'options' => ['regexp' => '/^(standard|express)$/']],
];
$daten = filter_input_array(INPUT_POST, $regeln) ?? [];
$fehler = [];
foreach ($regeln as $feld => $_) {
if (!array_key_exists($feld, $daten) || $daten[$feld] === false || $daten[$feld] === null) {
$fehler[] = $feld;
}
}
if ($fehler !== []) {
http_response_code(422);
// Изведете съобщение за грешка за всяко поле, не обработвайте входните данни
}
Обърнете внимание на стриктните сравнения с ===: валидна стойност 0 или празен низ не бива да се бърка с неуспех. Защо хлабавите сравнения тук са опасни, обяснява Type Juggling: хлабавите сравнения като риск за сигурността. Как PHP изобщо предоставя данните от заявката, описва Superglobals: $_GET, $_POST, $_FILES и др..
Клопки при имейл, URL и свободен текст
Някои филтри правят по-малко, отколкото подсказва името им:
- URL адреси:
FILTER_VALIDATE_URLприема и други схеми освен http и https. Проверете схемата след това сparse_url()спрямо фиксиран списък. Ако URL адресът се извлича от сървъра, важат допълнителни правила, вижте Предотвратяване на Server-Side Request Forgery в PHP. - Имейл адреси: Филтърът проверява формата, а не притежанието. Който трябва да е сигурен, изпраща връзка за потвърждение.
- Свободен текст: Имена, съобщения и адреси трудно могат смислено да се ограничат по знаци. Проверявайте дължината и кодирането на знаците, например с
mb_check_encoding($wert, 'UTF-8'), и разчитайте при изхода на кодиране. - Регулярни изрази: Без
^и$шаблонът проверява само дали се среща някъде в текста, а не дали цялата стойност съвпада. - Масиви вместо низове: Атакуващият може да изпрати масив вместо текст. Без проверка на типа това води до предупреждения или неочаквано поведение.
За фиксирани стойности за избор като сортиране, език или начин на доставка списък с разрешени стойности с in_array($wert, $erlaubt, true) или израз match е по-прост и по-строг от всеки регулярен израз.
Друга честа грешка засяга числа със значение, например цени, количества или отстъпки. Това, че една стойност е валидно число, не казва нищо дали е позволена в контекста. Отрицателно количество, отстъпка над стойността на стоката или цена, взета от формуляра вместо от базата данни, са класически логически грешки. Цените, правата и стойностите на състоянието затова винаги принадлежат на сървъра и никога в скрити полета на формуляри.
JSON, уебхукове и импорти: валидиране отвъд формулярите
Много входове днес вече не са формуляри, а JSON заявки от приложения, уебхукове от платежни услуги или файлове за импорт от доставчици. И там важи: първо се чете безопасно, после се проверява. OWASP съветва да се ограничат размерът и дълбочината на вложеност, преди данните изобщо да се прочетат. В PHP json_decode() с максимална дълбочина и JSON_THROW_ON_ERROR поема част от това:
<?php
$roh = file_get_contents('php://input', false, null, 0, 65536); // най-много 64 KB
try {
$daten = json_decode($roh, true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException) {
http_response_code(400);
exit;
}
if (!is_array($daten) || !is_string($daten['bestellnummer'] ?? null)) {
http_response_code(422);
exit;
}
При уебхуковете преди проверката на съдържанието стои въпросът дали съобщението наистина идва от посочената услуга. Ако услугата предлага подпис, проверете го и го сравнете с hash_equals(). При файлове за импорт проверявайте всеки ред като формуляр и при грешки прекъсвайте чисто, вместо да приемате половин записи.
Съобщенията за грешка трябва да назовават засегнатото поле, но да не връщат отхвърлената стойност нефилтрирана. Иначе в самото съобщение за грешка възниква ново място за изход, което отново трябва да се кодира.
Какво валидирането не може да постигне
Валидирането намалява повърхността за атака, но според OWASP не е главната защита срещу инжекции. Фамилно име с апостроф е валидно и въпреки това трябва да стигне безопасно до базата данни. Това става само с Prepared Statements, както показва наръчникът Предотвратяване на SQL инжекция в PHP. А валиден текст на коментар става безопасен едва с кодиране според контекста при изхода.
Правилната последователност следователно е: валидиране при получаването, параметризиране при обработката, кодиране при изхода. Допълнително преди това стоят ограниченията на размера: заявки, които са далеч твърде големи, уеб сървърът и PHP не бива изобщо да обработват изцяло. За това служат настройки като post_max_size, които след PHP2Shell така или иначе трябва да се прегледат.
Как помага PHP SOS конкретно?
- Отчитаме всички входове на Вашето приложение: формуляри, интерфейси, качвания, импорти и уебхукове.
- Определяме за всяко поле какво е разрешено и заменяме общите функции за почистване с ясни проверки.
- Проверяваме дали достъпите до базата данни и изходите са защитени независимо от валидирането.
- Документираме правилата така, че Вашият екип да може да ги продължава при нови функции.
Систематична проверка описва услугата Одит на сигурността и тест за проникване.
Често задавани въпроси
Каква е разликата между валидиране и почистване?
Валидирането проверява дали една стойност отговаря на очакванията и в противен случай я отхвърля. Почистването променя стойността, например като премахва знаци. За решения, свързани със сигурността, отхвърлянето почти винаги е по-добро, защото мълчаливо променена стойност може да породи нови грешки.
Все още ли може да се ползва FILTER_SANITIZE_STRING?
Филтърът е маркиран като остарял от PHP 8.1. Наръчникът на PHP препоръчва вместо това htmlspecialchars() при изхода. Съществуващият код трябва да преминете, защото остарели функции могат да отпаднат в бъдещи версии.
Достатъчно ли е FILTER_VALIDATE_URL, за да направи връзките сигурни?
Не. Филтърът проверява само формата на адреса. Според наръчника валиден URL не е задължително да започва с http, затова схемата трябва да проверите сами спрямо списък с разрешени стойности като https. Интернационализирани имена на домейни филтърът отхвърля.
Трябва ли да валидирам и данни от собствената си база данни?
При изхода те се смятат за ненадеждни и се кодират според контекста. Ново валидиране е смислено, когато данните първоначално са дошли отвън и сега се използват в нов контекст, например като име на файл или част от команда.
Помага ли валидирането срещу PHP2Shell?
Не. PHP2Shell засяга обработката на заявки в интерпретатора на PHP, преди кодът на Вашето приложение изобщо да се изпълни. Срещу това помага само актуализацията на PHP със следващ рестарт на всички PHP процеси.