Krótko: CSRF w PHP zapobiegasz, wymagając, aby każde żądanie, które coś zmienia, dostarczało tajny token powiązany z sesją, sprawdzany po stronie serwera za pomocą hash_equals(). Dodatkowo sprawdzasz nagłówki Origin lub Sec-Fetch-Site i ustawiasz ciasteczka sesji z SameSite. Żądania GET nigdy nie mogą zmieniać danych, a formularz logowania także wymaga ochrony.
Kliknięcie, którego nikt nie zauważa
Pracownica jest zalogowana w backendzie sklepu i otwiera w drugiej karcie obcą stronę. Ta strona wysyła w tle do backendu formularz, na przykład zmieniający adres e-mail albo zakładający konto. Przeglądarka automatycznie dołącza ciasteczko sesji. Dla aplikacji żądanie wygląda jak każde inne z własnego zespołu.
To jest Cross-Site Request Forgery, w skrócie CSRF: żądanie, które zalogowany użytkownik nieświadomie wysyła do aplikacji, której ufa. Atakujący nie widzi odpowiedzi, ale jej nie potrzebuje. Wystarczy, że akcja zostanie wykonana.
CSRF nie trafia na pierwsze strony tak jak PHP2Shell, ale w samodzielnie tworzonych aplikacjach PHP jest częstym ustaleniem. Frameworki takie jak Symfony i Laravel zawierają ochronę; luki powstają tam głównie wtedy, gdy wyłączono ją dla pojedynczych tras. Własne rozwiązania bez frameworka często nie mają jej wcale.
CSRF w PHP: które żądania trzeba chronić
Ochrony potrzebują wszystkie żądania zmieniające stan: zapisywanie, usuwanie, zamawianie, nadawanie uprawnień, zmiana ustawień, przesyłanie plików, wylogowanie. Wywołania odczytujące nie potrzebują tokenu, dopóki naprawdę tylko odczytują.
Stąd pierwsza zasada: żadnych zmian metodą GET. Linki takie jak loeschen.php?id=… obce strony mogą wywołać wyjątkowo łatwo, na przykład przez osadzony obraz. Akcje zmieniające dane należą do POST, PUT, PATCH lub DELETE.
Druga zasada: chronić także logowanie. W tzw. login CSRF ofiara zostaje niepostrzeżenie zalogowana na konto atakującego. Jeśli wpisze tam dane płatnicze lub dokumenty, trafią one do atakującego. Zapobiegają temu token w formularzu logowania i nowy identyfikator sesji po udanym zalogowaniu.
Wzorzec tokenu synchronizującego w PHP
Dla klasycznych aplikacji z sesjami OWASP zaleca wzorzec tokenu synchronizującego (Synchronizer Token): serwer generuje losowy token, zapisuje go w sesji i oczekuje go z powrotem w każdym formularzu zmieniającym dane. Ważne, aby token był przesyłany w formularzu lub we własnym nagłówku, a nie w ciasteczku.
<?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') ?>">
<!-- kolejne pola -->
</form>
hash_equals() porównuje w stałym czasie, dzięki czemu czas odpowiedzi nie zdradza prawidłowej wartości. Po zalogowaniu należy odnowić identyfikator sesji za pomocą session_regenerate_id(true) i wygenerować nowy token. Dlaczego random_bytes() jest właściwym źródłem i które funkcje się nie nadają, wyjaśnia artykuł Bezpieczne liczby losowe i tokeny w PHP.
Jeden token na sesję wystarcza w większości aplikacji. Tokeny na formularz lub na żądanie nieco zwiększają ochronę, ale prowadzą do problemów z kilkoma otwartymi kartami i przyciskiem Wstecz. Ważniejsze od częstotliwości zmiany jest to, aby token nigdy nie pojawiał się w adresach URL, gdzie może trafić do logów, historii przeglądarki i informacji Referrer przekazywanych obcym stronom.
Dla aplikacji bezstanowych bez sesji OWASP wskazuje jako alternatywę podpisane ciasteczko double-submit: token powiązany z sesją za pomocą HMAC. Prosty, niepodpisany wariant jest wyraźnie niezalecany.
Druga warstwa: Origin, Sec-Fetch-Site i SameSite
Nowoczesne przeglądarki wysyłają w żądaniach informacje o swoim pochodzeniu. Nagłówek Origin wskazuje stronę wywołującą, a Sec-Fetch-Site mówi, czy żądanie pochodzi z tej samej strony (same-origin), spokrewnionej (same-site) czy obcej (cross-site). Na tej podstawie można zbudować prostą dodatkową kontrolę:
<?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 podkreśla tu dwa punkty: pochodzenie trzeba porównywać w całości, aby podobnie brzmiące domeny się nie prześlizgnęły, a brakujący nagłówek lub wartość null nie może być uznawana za własne pochodzenie. Ponieważ starsze przeglądarki nie zawsze wysyłają te nagłówki, token pozostaje główną obroną.
Trzecią warstwą są ciasteczka sesji z SameSite. PHP obsługuje session.cookie_samesite od wersji 7.3, wartość domyślna jest pusta. Ustaw co najmniej Lax, dla czystych backendów często możliwy jest Strict. Pozostałe ustawienia ciasteczek opisuje poradnik Zabezpieczanie sesji w PHP.
Jeśli używasz CMS, zwykle ma on własne narzędzia. WordPress na przykład pracuje z tzw. nonce, które wtyczki włączają za pomocą funkcji takich jak wp_nonce_field() i check_admin_referer(). Luki powstają w praktyce głównie w rozszerzeniach, które przy pojedynczych akcjach zapominają o takich kontrolach. Dlatego szczególnie sprawdź wtyczki napisane samodzielnie lub mało rozpowszechnione.
W przypadku interfejsów używanych przez przeglądarki z innych domen dodatkowo rozstrzyga konfiguracja CORS. Jeśli serwer zwraca każde pytające pochodzenie jako dozwolone i jednocześnie dopuszcza ciasteczka, sam unieważnia ochronę przeglądarki. OWASP zaleca otwieranie CORS tylko dla jawnie zaufanych źródeł; zbiorcze zezwolenie dla wszystkich subdomen staje się problemem, gdy tylko jedna z nich wpadnie w obce ręce.
Lista kontrolna dla istniejących aplikacji
W przypadku rozrastających się projektów pomaga systematyczny przegląd:
- Spisać wszystkie trasy i skrypty, które zmieniają dane, także punkty końcowe Ajax i przesyłanie plików.
- Sprawdzić, czy używają GET, i przestawić na POST lub odpowiednie metody.
- Dla każdej trasy zmieniającej dane wykazać, że token jest sprawdzany; wyjątki we frameworku wyszukać celowo.
- Logowanie, wylogowanie i zmianę hasła przetestować osobno.
- Uzupełnić
session.cookie_samesitei kontrolę pochodzenia. - Usunąć luki XSS, ponieważ wstrzyknięty kod może odczytać każdy token. Postępowanie opisuje artykuł Zapobieganie Cross-Site Scripting w PHP.
Ochrona przed CSRF sprawdzana tylko we frontendzie za pomocą JavaScript jest bezskuteczna. Kontrola musi odbywać się na serwerze, zanim akcja zostanie wykonana.
Jak konkretnie pomaga PHP SOS?
- Spisujemy wszystkie trasy zmieniające dane w Państwa aplikacji PHP i sprawdzamy, gdzie brakuje tokenu lub został wyłączony.
- Wprowadzamy centralny punkt kontroli, zamiast rozpraszać sprawdzanie tokenów po wielu skryptach.
- Uzupełniamy kontrolę pochodzenia i ustawienia SameSite, nie zakłócając procesów płatności ani logowania.
- Po zmianie testujemy ponownie i dokumentujemy wyniki.
Pełną kontrolę Państwa aplikacji opisuje usługa audyt bezpieczeństwa i test penetracyjny.
Częste pytania
Czy SameSite=Lax wystarczy jako ochrona przed CSRF?
Nie. Ciasteczka SameSite są według OWASP dodatkową warstwą, a nie zamiennikiem. Lax nadal wysyła ciasteczka przy zwykłych wywołaniach stron metodą GET, a ataki z subdomen tej samej witryny nie są objęte ochroną.
Czy API JSON potrzebują tokenu CSRF?
Jeśli API uwierzytelnia za pomocą ciasteczek, tak lub równoważnej kontroli. Alternatywą są własne nagłówki żądań, na które przeglądarki w przypadku obcych stron pozwalają dopiero po wstępnej kontroli CORS. Wtedy zgoda CORS może jednak obejmować wyłącznie jawnie zaufane źródła.
Czy formularz logowania także trzeba chronić?
Tak. W przypadku login CSRF ofiara zostaje niepostrzeżenie zalogowana na obce konto i wpisuje tam własne dane. Zapobiegają temu token w formularzu logowania i nowa sesja po zalogowaniu.
Czy ochrona przed CSRF chroni także przed XSS?
Odwrotnie: luka XSS unieważnia każdą ochronę przed CSRF, ponieważ wstrzyknięty kod w imieniu strony może odczytać i dołączyć ważne tokeny. Obie podatności trzeba usuwać oddzielnie.