Накратко: CSRF в PHP предотвратявате, като всяка заявка, която променя нещо, трябва да носи таен токен, свързан със сесията, който проверявате на сървъра с hash_equals(). Допълнително проверявате заглавните части Origin или Sec-Fetch-Site и задавате сесийните бисквитки със SameSite. GET заявките никога не бива да променят данни, а формулярът за вход също се нуждае от защита.
Едно щракване, което никой не забелязва
Служителка е влязла в бекенда на магазина и отваря в друг раздел чужда страница. Тази страница изпраща на заден план формуляр към бекенда, например за смяна на имейл адрес или за създаване на акаунт. Браузърът прикачва автоматично сесийната бисквитка. За приложението заявката изглежда като всяка друга от собствения екип.
Това е Cross-Site Request Forgery, накратко CSRF: заявка, която вписан потребител неволно изпраща към приложение, на което се доверява. Атакуващият не вижда отговора, но и не се нуждае от него. Достатъчно е действието да бъде изпълнено.
CSRF не стои в заглавията като PHP2Shell, но е честа констатация в самостоятелно разработени PHP приложения. Фреймуърци като Symfony и Laravel носят защита; пропуските там възникват най-вече там, където тя е изключена за отделни маршрути. Собствените разработки без фреймуърк често нямат такава изобщо.
CSRF в PHP: кои заявки трябва да се защитят
Защита изискват всички заявки, които променят състояние: запис, изтриване, поръчване, даване на права, смяна на настройки, качване на файлове, изход от системата. Заявките за четене не се нуждаят от токен, стига наистина само да четат.
От това следва първото правило: без промяна чрез GET. Връзки като loeschen.php?id=… са особено лесни за задействане от чужди страници, например чрез вградено изображение. Променящите действия принадлежат на POST, PUT, PATCH или DELETE.
Второто правило: защитавайте и автентикацията. При т.нар. Login CSRF жертвата бива вписана незабелязано в акаунт на атакуващия. Ако въведе там платежни данни или документи, те попадат при атакуващия. Токен във формуляра за вход и нов идентификатор на сесията след успешен вход предотвратяват това.
Моделът Synchronizer Token в PHP
За класически приложения със сесии OWASP препоръчва модела Synchronizer Token: сървърът създава случаен токен, съхранява го в сесията и го очаква обратно във всеки променящ формуляр. Важно е токенът да се изпраща във формуляра или в отделна заглавна част, а не в бисквитка.
<?php
declare(strict_types=1);
function csrfToken(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrfPruefen(): void
{
$gesendet = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!is_string($gesendet) || !hash_equals(csrfToken(), $gesendet)) {
http_response_code(403);
exit('Anfrage abgelehnt.');
}
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
csrfPruefen();
}
?>
<form method="post" action="/konto/email">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(csrfToken(), ENT_QUOTES, 'UTF-8') ?>">
<!-- допълнителни полета -->
</form>
hash_equals() сравнява за константно време, така че времето за отговор не издава нищо за правилната стойност. След вход трябва да обновите идентификатора на сесията със session_regenerate_id(true) и да създадете нов токен. Защо random_bytes() е правилният източник и кои функции са неподходящи, обяснява Сигурни случайни числа и токени в PHP.
Един токен на сесия е достатъчен за повечето приложения. Токени за всеки формуляр или за всяка заявка повишават малко защитата, но водят до проблеми при няколко отворени раздела и бутона „Назад“. По-важно от честотата на смяна е токенът никога да не се появява в URL адреси, където може да попадне в журнали, в историята на браузъра и в данните за препращащата страница (Referrer) към чужди страници.
За приложения без състояние и без сесия OWASP посочва като алтернатива подписаната Double-Submit бисквитка: токен, свързан със сесията чрез HMAC. Простият, неподписан вариант изрично не се препоръчва.
Втори слой: Origin, Sec-Fetch-Site и SameSite
Съвременните браузъри изпращат при заявки информация за произхода си. Заглавната част Origin назовава извикващата страница, Sec-Fetch-Site казва дали заявката идва от същата страница (same-origin), от сродна (same-site) или от чужда (cross-site). От това може да се изгради проста допълнителна проверка:
<?php
$erlaubteHerkunft = 'https://www.example.de';
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? null;
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
$verdaechtig = ($fetchSite !== null && $fetchSite !== 'same-origin')
|| ($fetchSite === null && $origin !== null && $origin !== $erlaubteHerkunft);
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $verdaechtig) {
http_response_code(403);
exit;
}
OWASP подчертава две точки: произходът трябва да се сравнява изцяло, за да не минат домейни с подобно звучене, а липсваща заглавна част или стойността null не бива да се счита за собствен произход. Тъй като по-старите браузъри не винаги изпращат заглавните части, токенът остава главната защита.
Третият слой са сесийните бисквитки със SameSite. PHP поддържа session.cookie_samesite от версия 7.3, стандартната стойност е празна. Задайте поне Lax, за чисти бекенди често е възможно Strict. Останалите настройки за бисквитките описва наръчникът Защита на сесиите в PHP.
Ако ползвате CMS, то обикновено носи собствени инструменти. WordPress например работи с т.нар. nonces, които добавките включват чрез функции като wp_nonce_field() и check_admin_referer(). На практика пропуските възникват най-вече в разширения, които забравят такива проверки при отделни действия. Затова проверявайте особено добавките, писани на ръка или малко разпространени.
При интерфейси, които браузърите ползват от други домейни, допълнително решава CORS конфигурацията. Ако сървърът връща като разрешен всеки заявяващ произход и същевременно допуска бисквитки, той сам обезсмисля защитата на браузъра. OWASP препоръчва CORS да се отваря само за изрично надеждни произходи; общо разрешение за всички поддомейни става проблем, щом някой от тях попадне в чужди ръце.
Контролен списък за съществуващи приложения
При разраснали се проекти помага систематичен преглед:
- Отчетете всички маршрути и скриптове, които променят данни, включително Ajax крайни точки и качване на файлове.
- Проверете дали за тях се ползва GET и преминете към POST или подходящи методи.
- За всеки променящ маршрут докажете, че токенът се проверява; търсете целенасочено изключения във фреймуърка.
- Тествайте отделно вход, изход и смяна на парола.
- Добавете
session.cookie_samesiteи проверката на произхода. - Отстранете XSS уязвимостите, защото внедреният код може да прочете всеки токен. Подходът описва Предотвратяване на Cross-Site Scripting в PHP.
CSRF защита, която се проверява само във фронтенда чрез JavaScript, е безполезна. Проверката трябва да се извършва на сървъра, преди действието да бъде изпълнено.
Как помага PHP SOS конкретно?
- Отчитаме всички променящи маршрути на Вашето PHP приложение и проверяваме къде липсва токен или е изключен.
- Въвеждаме централна точка за проверка, вместо проверките на токени да са разпръснати из много скриптове.
- Добавяме проверка на произхода и настройки за SameSite, без да нарушаваме процесите на плащане или вход.
- След преминаването тестваме отново и документираме резултатите.
Пълна проверка на Вашето приложение описва услугата Одит на сигурността и тест за проникване.
Често задавани въпроси
Достатъчно ли е SameSite=Lax като CSRF защита?
Не. Според OWASP SameSite бисквитките са допълнителен слой, а не заместител. Lax продължава да изпраща бисквитки при обикновени извиквания на страници чрез GET, а атаките от поддомейни на същия уебсайт не са покрити.
Трябва ли JSON API да имат CSRF токен?
Ако API удостоверява с бисквитки, да, или равностойна проверка. Алтернатива са собствени заглавни части на заявката, които браузърите разрешават на чужди страници само след предварителна CORS проверка. Тогава CORS разрешението обаче може да включва само изрично надеждни произходи.
Трябва ли да се защитава и формулярът за вход?
Да. При Login CSRF жертвата бива вписана незабелязано в чужд акаунт и въвежда там собствени данни. Токен във формуляра за вход и нова сесия след автентикацията предотвратяват това.
Защитава ли CSRF защитата и от XSS?
Точно обратното: една XSS уязвимост обезсмисля всяка CSRF защита, защото внедреният код може от името на страницата да прочете валидни токени и да ги изпрати. И двете слабости трябва да се отстраняват поотделно.