Ukratko: CSRF u PHP-u sprječavate tako da svaki zahtjev koji nešto mijenja mora sa sobom donijeti tajni token vezan uz sesiju, koji na poslužitelju provjeravate pomoću hash_equals(). Uz to provjeravate zaglavlja Origin ili Sec-Fetch-Site i postavljate kolačiće sesije s SameSite. GET zahtjevi nikad ne smiju mijenjati podatke, a i obrazac za prijavu treba zaštitu.
Klik koji nitko ne primijeti
Zaposlenica je prijavljena u backend shopa i u drugoj kartici otvara tuđu stranicu. Ta stranica u pozadini šalje obrazac backendu, primjerice za promjenu adrese e-pošte ili stvaranje računa. Preglednik automatski dodaje kolačić sesije. Aplikaciji zahtjev izgleda kao i svaki drugi iz vlastitog tima.
To je Cross-Site Request Forgery, skraćeno CSRF: zahtjev koji prijavljeni korisnik nesvjesno šalje aplikaciji u koju ima povjerenja. Napadač ne vidi odgovor, ali mu nije ni potreban. Dovoljno je da se radnja izvrši.
CSRF se ne pojavljuje u naslovima kao PHP2Shell, ali je u PHP aplikacijama razvijenim u vlastitoj izvedbi čest nalaz. Frameworci poput Symfonyja i Laravela donose zaštitu; ranjivosti nastaju ponajprije tamo gdje je za pojedine rute isključena. Vlastite izvedbe bez frameworka često je uopće nemaju.
CSRF u PHP-u: koje zahtjeve treba zaštititi
Zaštitu trebaju svi zahtjevi koji mijenjaju stanje: spremanje, brisanje, naručivanje, dodjela prava, promjena postavki, upload datoteka, odjava. Pozivi koji samo čitaju ne trebaju token, sve dok doista samo čitaju.
Iz toga proizlazi prvo pravilo: Nikakve promjene putem GET-a. Poveznice poput loeschen.php?id=… tuđe stranice mogu osobito lako pokrenuti, primjerice preko ugrađene slike. Radnje koje mijenjaju pripadaju u POST, PUT, PATCH ili DELETE.
Drugo pravilo: Zaštititi i prijavu. Kod takozvanog login CSRF-a žrtva se neprimjetno prijavi u račun napadača. Ako tamo unese podatke za plaćanje ili dokumente, oni završavaju kod napadača. Token u obrascu za prijavu i novi ID sesije nakon uspješne prijave to sprječavaju.
Uzorak Synchronizer tokena u PHP-u
Za klasične aplikacije sa sesijama OWASP preporučuje uzorak Synchronizer tokena: poslužitelj stvara slučajni token, sprema ga u sesiju i očekuje ga natrag u svakom obrascu koji mijenja podatke. Važno je da se token šalje u obrascu ili u vlastitom zaglavlju, a ne u kolačiću.
<?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') ?>">
<!-- ostala polja -->
</form>
hash_equals() uspoređuje u konstantnom vremenu, pa vrijeme odgovora ne otkriva ništa o ispravnoj vrijednosti. Nakon prijave trebali biste obnoviti ID sesije pomoću session_regenerate_id(true) i stvoriti novi token. Zašto je random_bytes() ispravan izvor i koje su funkcije neprikladne, objašnjava članak Sigurni slučajni brojevi i tokeni u PHP-u.
Jedan token po sesiji dovoljan je za većinu aplikacija. Tokeni po obrascu ili po zahtjevu malo povećavaju zaštitu, ali dovode do problema s više otvorenih kartica i gumbom za povratak. Važnije od učestalosti promjene je da se token nikad ne pojavljuje u URL-ovima, gdje može završiti u zapisnicima, povijesti preglednika i u podacima o referreru prema tuđim stranicama.
Za aplikacije bez stanja i bez sesije OWASP kao alternativu navodi potpisani Double-Submit kolačić: token vezan za sesiju pomoću HMAC-a. Jednostavna, nepotpisana varijanta izričito se ne preporučuje.
Drugi sloj: Origin, Sec-Fetch-Site i SameSite
Moderni preglednici pri zahtjevima šalju podatke o svojem podrijetlu. Zaglavlje Origin navodi stranicu koja poziva, a Sec-Fetch-Site kaže dolazi li zahtjev s iste stranice (same-origin), srodne (same-site) ili tuđe (cross-site). Iz toga se može sastaviti jednostavna dodatna provjera:
<?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 pritom naglašava dvije točke: podrijetlo se mora uspoređivati u cijelosti kako domene sličnog zvuka ne bi prošle, a nedostajuće zaglavlje ili vrijednost null ne smiju vrijediti kao vlastito podrijetlo. Budući da stariji preglednici zaglavlja ne šalju uvijek, token ostaje glavna obrana.
Treći sloj su kolačići sesije s SameSite. PHP podržava session.cookie_samesite od verzije 7.3, a zadana vrijednost je prazna. Postavite najmanje Lax, za čiste backende često je moguć Strict. Ostale postavke kolačića opisuje vodič Osigurati sesije u PHP-u.
Ako koristite CMS, on obično donosi vlastite alate. WordPress, primjerice, radi s takozvanim nonceovima, koje dodaci uključuju funkcijama poput wp_nonce_field() i check_admin_referer(). Ranjivosti u praksi nastaju ponajprije u proširenjima koja takve provjere zaborave kod pojedinih radnji. Zato osobito provjerite vlastoručno napisane ili malo raširene dodatke.
Kod sučelja koja preglednici koriste s drugih domena dodatno odlučuje CORS konfiguracija. Ako poslužitelj svako podrijetlo koje šalje zahtjev vraća kao dopušteno, a istodobno dopušta kolačiće, sam poništava zaštitu preglednika. OWASP preporučuje CORS otvoriti samo za izričito pouzdana podrijetla; paušalno odobrenje svih poddomena postaje problem čim jedna od njih dospije u tuđe ruke.
Kontrolni popis za postojeće aplikacije
Kod razvijenih projekata pomaže sustavan pregled:
- Popisati sve rute i skripte koje mijenjaju podatke, uključujući Ajax krajnje točke i uploade datoteka.
- Provjeriti koristi li se za to GET i prijeći na POST ili odgovarajuće metode.
- Za svaku rutu koja mijenja dokazati da se token provjerava; iznimke u frameworku tražiti ciljano.
- Zasebno testirati prijavu, odjavu i promjenu lozinke.
- Dopuniti
session.cookie_samesitei provjeru podrijetla. - Ukloniti XSS ranjivosti, jer ubačeni kod može pročitati svaki token. Postupak opisuje članak Spriječiti Cross-Site Scripting u PHP-u.
CSRF zaštita koja se provjerava samo u frontendu pomoću JavaScripta nema učinka. Provjera se mora odvijati na poslužitelju, prije izvršenja radnje.
Kako PHP SOS konkretno pomaže?
- Bilježimo sve rute Vaše PHP aplikacije koje mijenjaju podatke i provjeravamo gdje token nedostaje ili je isključen.
- Ugrađujemo središnju točku provjere umjesto da se provjere tokena raspoređuju po mnogim skriptama.
- Dopunjujemo provjeru podrijetla i SameSite postavke, a da ne smetamo procesima plaćanja ili prijave.
- Nakon prijelaza ponovno testiramo i dokumentiramo rezultate.
Potpunu provjeru Vaše aplikacije opisuje usluga Sigurnosni audit i penetracijski test.
Česta pitanja
Je li SameSite=Lax dovoljan kao CSRF zaštita?
Ne. SameSite kolačići prema OWASP-u su dodatni sloj, a ne zamjena. Lax i dalje šalje kolačiće pri običnim pozivima stranica metodom GET, a napadi s poddomena iste web-stranice nisu pokriveni.
Trebaju li JSON API-ji CSRF token?
Ako API autentificira pomoću kolačića, da, ili jednakovrijednu provjeru. Alternativa su vlastita zaglavlja zahtjeva koja preglednici s tuđih stranica dopuštaju tek nakon CORS prethodne provjere. Tada CORS odobrenje smije sadržavati samo izričito pouzdana podrijetla.
Mora li se zaštititi i obrazac za prijavu?
Da. Kod login CSRF-a žrtva se neprimjetno prijavi u tuđi račun i tamo unosi vlastite podatke. Token u obrascu za prijavu i nova sesija nakon prijave to sprječavaju.
Štiti li CSRF zaštita i od XSS-a?
Obrnuto: XSS ranjivost poništava svaku CSRF zaštitu, jer ubačeni kod u ime stranice može pročitati valjane tokene i poslati ih. Obje se ranjivosti moraju ukloniti zasebno.