Stručně: CSRF v PHP zabráníte tak, že každý požadavek, který něco mění, musí přinést tajný token vázaný na relaci, který na serveru ověříte pomocí hash_equals(). Dále kontrolujete hlavičky Origin nebo Sec-Fetch-Site a cookies relace nastavujete s SameSite. Požadavky GET nesmějí nikdy měnit data a ochranu potřebuje i přihlašovací formulář.
Kliknutí, kterého si nikdo nevšimne
Zaměstnankyně je přihlášena do backendu e-shopu a ve druhé záložce otevře cizí stránku. Ta na pozadí odešle backendu formulář, například ke změně e-mailové adresy nebo založení účtu. Prohlížeč automaticky připojí cookie relace. Pro aplikaci vypadá požadavek jako každý jiný z vlastního týmu.
To je Cross-Site Request Forgery, zkráceně CSRF: požadavek, který přihlášený uživatel nevědomky odešle aplikaci, které důvěřuje. Útočník odpověď nevidí, ale ani ji nepotřebuje. Stačí, že se akce provede.
CSRF není v titulcích jako PHP2Shell, ale ve vlastních aplikacích v PHP je to častý nález. Frameworky jako Symfony a Laravel ochranu obsahují; mezery vznikají hlavně tam, kde byla pro jednotlivé routy vypnuta. Vlastní vývoj bez frameworku často nemá žádnou.
CSRF v PHP: které požadavky je třeba chránit
Ochranu potřebují všechny požadavky, které mění stav: ukládání, mazání, objednávání, přidělování práv, změna nastavení, nahrávání souborů, odhlášení. Čtecí požadavky token nepotřebují, pokud skutečně jen čtou.
Z toho plyne první pravidlo: Žádná změna přes GET. Odkazy jako loeschen.php?id=… lze z cizích stránek vyvolat obzvlášť snadno, například přes vložený obrázek. Měnící akce patří do POST, PUT, PATCH nebo DELETE.
Druhé pravidlo: Chraňte i přihlášení. U takzvaného login CSRF je oběť nepozorovaně přihlášena do účtu útočníka. Pokud tam zadá platební údaje nebo dokumenty, skončí u útočníka. Token v přihlašovacím formuláři a nové ID relace po úspěšném přihlášení tomu zabrání.
Vzor synchronizer token v PHP
Pro klasické aplikace s relacemi doporučuje OWASP vzor synchronizer token: server vytvoří náhodný token, uloží ho do relace a očekává ho zpět v každém měnícím formuláři. Důležité je, aby se token posílal ve formuláři nebo ve vlastní hlavičce, ne v cookie.
<?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') ?>">
<!-- další pole -->
</form>
hash_equals() porovnává v konstantním čase, takže doba odpovědi neprozradí nic o správné hodnotě. Po přihlášení byste měli ID relace obnovit pomocí session_regenerate_id(true) a vytvořit nový token. Proč je random_bytes() správným zdrojem a které funkce se nehodí, vysvětluje článek Bezpečná náhodná čísla a tokeny v PHP.
Pro většinu aplikací stačí jeden token na relaci. Tokeny pro každý formulář nebo požadavek ochranu mírně zvyšují, ale vedou k problémům s více otevřenými záložkami a tlačítkem Zpět. Důležitější než četnost změny je, aby se token nikdy neobjevil v URL, kde může skončit v lozích, v historii prohlížeče a v údajích Referrer pro cizí stránky.
Pro bezstavové aplikace bez relace uvádí OWASP jako alternativu podepsané double-submit cookie: token vázaný na sezení pomocí HMAC. Jednoduchá, nepodepsaná varianta se výslovně nedoporučuje.
Druhá vrstva: Origin, Sec-Fetch-Site a SameSite
Moderní prohlížeče posílají u požadavků informace o svém původu. Hlavička Origin uvádí volající stránku, Sec-Fetch-Site říká, zda požadavek pochází ze stejné stránky (same-origin), příbuzné (same-site) nebo cizí (cross-site). Z toho lze sestavit jednoduchou doplňkovou kontrolu:
<?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 zdůrazňuje dva body: původ se musí porovnávat celý, aby neprošly podobně znějící domény, a chybějící hlavička nebo hodnota null se nesmí považovat za vlastní původ. Protože starší prohlížeče hlavičky nevždy posílají, zůstává hlavní obranou token.
Třetí vrstvou jsou cookies relace s SameSite. PHP podporuje session.cookie_samesite od verze 7.3, výchozí hodnota je prázdná. Nastavte alespoň Lax, pro čisté backendy je často možné Strict. Ostatní nastavení cookies popisuje příručka Zabezpečení relací v PHP.
Pokud používáte CMS, obvykle přináší vlastní nástroje. WordPress například pracuje s takzvanými nonces, které pluginy zapojují pomocí funkcí jako wp_nonce_field() a check_admin_referer(). Mezery vznikají v praxi hlavně v rozšířeních, která takové kontroly u jednotlivých akcí zapomenou. Zkontrolujte proto zvlášť vlastnoručně psané nebo málo rozšířené pluginy.
U rozhraní, která prohlížeče používají z jiných domén, rozhoduje navíc konfigurace CORS. Pokud server vrací každý žádající původ jako povolený a zároveň povoluje cookies, sám vyřadí ochranu prohlížeče. OWASP doporučuje otevřít CORS jen pro výslovně důvěryhodné původy; plošné povolení všech subdomén se stává problémem, jakmile se jedna z nich dostane do cizích rukou.
Kontrolní seznam pro existující aplikace
U vyrostlých projektů pomáhá systematická prohlídka:
- Zachyťte všechny routy a skripty, které mění data, včetně koncových bodů Ajax a nahrávání souborů.
- Zkontrolujte, zda se pro ně používá GET, a převeďte je na POST nebo vhodné metody.
- U každé měnící routy doložte, že se token ověřuje; cíleně hledejte výjimky ve frameworku.
- Přihlášení, odhlášení a změnu hesla otestujte samostatně.
- Doplňte
session.cookie_samesitea kontrolu původu. - Odstraňte zranitelnosti XSS, protože vložený kód může přečíst každý token. Postup popisuje stránka Prevence Cross-Site Scripting v PHP.
Ochrana CSRF, která se kontroluje jen ve frontendu pomocí JavaScriptu, je bez účinku. Kontrola musí proběhnout na serveru dříve, než se akce provede.
Jak konkrétně pomáhá PHP SOS?
- Zaznamenáme všechny měnící routy vaší aplikace v PHP a zkontrolujeme, kde token chybí nebo byl vypnut.
- Zavedeme centrální kontrolní místo místo rozprostření kontrol tokenu do mnoha skriptů.
- Doplníme kontrolu původu a nastavení SameSite, aniž bychom narušili platební nebo přihlašovací procesy.
- Po změně znovu otestujeme a výsledky zdokumentujeme.
Úplnou kontrolu vaší aplikace popisuje služba Bezpečnostní audit a penetrační test.
Časté dotazy
Stačí SameSite=Lax jako ochrana proti CSRF?
Ne. Cookies SameSite jsou podle OWASP další vrstva, ne náhrada. Lax dál posílá cookies při běžných zobrazeních stránky metodou GET a útoky ze subdomén téhož webu nepokrývá.
Potřebují API s JSON token CSRF?
Pokud API ověřuje pomocí cookies, ano, nebo rovnocennou kontrolu. Alternativou jsou vlastní hlavičky požadavku, které prohlížeče u cizích stránek povolí jen po předběžné kontrole CORS. Povolení CORS pak ale smí obsahovat pouze výslovně důvěryhodné původy.
Musí být chráněn i přihlašovací formulář?
Ano. U login CSRF je oběť nepozorovaně přihlášena do cizího účtu a zadává tam vlastní data. Token v přihlašovacím formuláři a nová relace po přihlášení tomu zabrání.
Chrání ochrana CSRF i před XSS?
Naopak: zranitelnost XSS vyřadí jakoukoli ochranu CSRF, protože vložený kód může jménem stránky číst platné tokeny a posílat je s požadavkem. Obě slabiny je nutné odstranit samostatně.