Trumpai: CSRF PHP užkertate kelią taip, kad kiekviena užklausa, kuri ką nors keičia, turi atsinešti slaptą, su sesija susietą žetoną, kurį serveryje tikrinate su hash_equals(). Papildomai tikrinate antraštes Origin ar Sec-Fetch-Site ir sesijos slapukus nustatote su SameSite. GET užklausos niekada neturi keisti duomenų, o prisijungimo forma taip pat reikalauja apsaugos.
Paspaudimas, kurio niekas nepastebi
Darbuotoja prisijungusi parduotuvės užpakalinėje dalyje ir antroje kortelėje atidaro svetimą puslapį. Šis puslapis fone nusiunčia formą į užpakalinę dalį, pavyzdžiui, el. pašto adreso keitimui ar paskyros sukūrimui. Naršyklė automatiškai prideda sesijos slapuką. Programai užklausa atrodo kaip bet kuri kita iš savos komandos.
Tai Cross-Site Request Forgery, trumpai CSRF: užklausa, kurią prisijungęs naudotojas nesąmoningai siunčia programai, kuria pasitiki. Užpuolikas atsakymo nemato, bet jo ir nereikia. Pakanka, kad veiksmas būtų įvykdytas.
CSRF neatsiduria antraštėse kaip PHP2Shell, bet savo kurtose PHP programose tai dažnas radinys. Karkasai, tokie kaip Symfony ir Laravel, turi apsaugą; spragos atsiranda ten, kur ji atskiriems maršrutams išjungta. Savos kūrybos sprendimai be karkaso dažnai neturi jos visai.
CSRF PHP: kurias užklausas reikia apsaugoti
Apsaugos reikia visoms užklausoms, keičiančioms būseną: išsaugojimui, ištrynimui, užsakymui, teisių suteikimui, nustatymų keitimui, failų įkėlimui, atsijungimui. Skaitymo kreipiniams žetono nereikia, kol jie iš tikrųjų tik skaito.
Iš to išplaukia pirma taisyklė: jokių pakeitimų per GET. Nuorodas kaip loeschen.php?id=… svetimi puslapiai gali ypač lengvai iškviesti, pavyzdžiui, per įterptą paveikslėlį. Keičiantys veiksmai turi būti POST, PUT, PATCH ar DELETE.
Antra taisyklė: saugokite ir prisijungimą. Vadinamuoju prisijungimo CSRF auka nepastebimai prijungiama prie užpuoliko paskyros. Jei ten suveda mokėjimo duomenis ar dokumentus, jie patenka užpuolikui. Žetonas prisijungimo formoje ir naujas sesijos ID po sėkmingo prisijungimo tai užkerta.
Sinchronizavimo žetono šablonas PHP
Klasikinėms programoms su sesijomis OWASP rekomenduoja sinchronizavimo žetono šabloną: serveris sukuria atsitiktinį žetoną, išsaugo jį sesijoje ir tikisi jo atgal kiekvienoje keičiančioje formoje. Svarbu, kad žetonas būtų siunčiamas forma ar atskira antraštė, o ne slapuku.
<?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') ?>">
<!-- kiti laukai -->
</form>
hash_equals() lygina pastoviu laiku, todėl atsakymo laikas neišduoda teisingos reikšmės. Po prisijungimo sesijos ID turėtumėte atnaujinti su session_regenerate_id(true) ir sukurti naują žetoną. Kodėl random_bytes() yra tinkamas šaltinis ir kurios funkcijos netinka, aiškina Saugūs atsitiktiniai skaičiai ir žetonai PHP.
Daugumai programų pakanka vieno žetono sesijai. Žetonai kiekvienai formai ar užklausai apsaugą šiek tiek padidina, bet sukelia problemų su keliomis atvertomis kortelėmis ir mygtuku „Atgal“. Svarbiau už keitimo dažnį, kad žetonas niekada nepasirodytų URL, kur jis gali patekti į žurnalus, naršyklės istoriją ir referrer duomenis svetimiems puslapiams.
Būsenos neturinčioms programoms be sesijos OWASP kaip alternatyvą nurodo pasirašytą Double-Submit slapuką: žetoną, per HMAC susietą su sesija. Paprastas, nepasirašytas variantas aiškiai nerekomenduojamas.
Antras sluoksnis: Origin, Sec-Fetch-Site ir SameSite
Šiuolaikinės naršyklės užklausų metu siunčia informaciją apie savo kilmę. Antraštė Origin nurodo kviečiantį puslapį, Sec-Fetch-Site sako, ar užklausa ateina iš to paties puslapio (same-origin), giminingo (same-site) ar svetimo (cross-site). Iš to galima sudaryti paprastą papildomą patikrą:
<?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 čia pabrėžia du dalykus: kilmė turi būti lyginama pilnai, kad panašiai skambantys domenai nepraslystų, o trūkstama antraštė ar reikšmė null negali būti laikoma savo kilme. Kadangi senesnės naršyklės antraščių ne visada siunčia, žetonas lieka pagrindine gynyba.
Trečias sluoksnis yra sesijos slapukai su SameSite. PHP session.cookie_samesite palaiko nuo 7.3 versijos, numatytoji reikšmė tuščia. Nustatykite bent Lax, grynai užpakalinėms dalims dažnai galima Strict. Kitus slapukų nustatymus aprašo vadovas Sesijų PHP apsauga.
Jei naudojate CMS, jis dažniausiai turi savų įrankių. WordPress, pavyzdžiui, dirba su vadinamaisiais nonce, kuriuos papildiniai įtraukia funkcijomis kaip wp_nonce_field() ir check_admin_referer(). Praktikoje spragos atsiranda daugiausia plėtiniuose, kurie tokius patikrinimus pamiršta atskiriems veiksmams. Todėl ypač patikrinkite savarankiškai parašytus ar mažai paplitusius papildinius.
Sąsajoms, kurias naršyklės naudoja iš kitų domenų, papildomai lemia CORS konfigūracija. Jei serveris kiekvieną kreipiančią kilmę grąžina kaip leidžiamą ir kartu leidžia slapukus, jis pats išjungia naršyklės apsaugą. OWASP rekomenduoja CORS atverti tik aiškiai patikimoms kilmėms; bendras visų subdomenų leidimas tampa problema, kai vienas iš jų patenka į svetimas rankas.
Esamų programų kontrolinis sąrašas
Išaugusiems projektams padeda sistemingas peržiūrėjimas:
- Surinkti visus maršrutus ir scenarijus, keičiančius duomenis, taip pat Ajax galinius taškus ir failų įkėlimus.
- Patikrinti, ar jiems naudojamas GET, ir perjungti į POST ar tinkamus metodus.
- Kiekvienam keičiančiam maršrutui įrodyti, kad žetonas tikrinamas; išimčių karkase ieškoti tikslingai.
- Prisijungimą, atsijungimą ir slaptažodžio keitimą išbandyti atskirai.
- Papildyti
session.cookie_samesiteir kilmės tikrinimą. - Pašalinti XSS spragas, nes įterptas kodas gali perskaityti bet kurį žetoną. Eigą aprašo Cross-Site Scripting PHP prevencija.
CSRF apsauga, tikrinama tik priekinėje dalyje per JavaScript, yra bevertė. Patikra turi vykti serveryje, prieš įvykdant veiksmą.
Kaip PHP SOS padeda konkrečiai?
- Surenkame visus jūsų PHP programos keičiančius maršrutus ir tikriname, kur žetono trūksta ar jis išjungtas.
- Įdiegiame centrinį patikros tašką, užuot paskirstę žetonų patikras per daugybę scenarijų.
- Papildome kilmės tikrinimą ir SameSite nustatymus netrikdydami mokėjimo ar prisijungimo eigų.
- Po perėjimo išbandome iš naujo ir dokumentuojame rezultatus.
Pilną jūsų programos patikrą aprašo paslauga Saugumo auditas ir įsiskverbimo testas.
Dažni klausimai
Ar SameSite=Lax kaip CSRF apsauga pakanka?
Ne. Pasak OWASP, SameSite slapukai yra papildomas sluoksnis, o ne pakaitalas. Lax slapukus vis dar siunčia įprastų puslapio kreipinių per GET metu, o atakos iš tos pačios svetainės subdomenų neapimamos.
Ar JSON API reikia CSRF žetono?
Jei API autentifikuoja slapukais, taip arba lygiaverčio patikrinimo. Alternatyva yra savos užklausos antraštės, kurias naršyklės svetimiems puslapiams leidžia tik po išankstinio CORS patikrinimo. Tačiau tuomet CORS leidimas turi apimti tik aiškiai patikimas kilmes.
Ar reikia apsaugoti ir prisijungimo formą?
Taip. Su prisijungimo CSRF auka nepastebimai prijungiama prie svetimos paskyros ir ten suveda savo duomenis. Žetonas prisijungimo formoje ir nauja sesija po prisijungimo to išvengia.
Ar CSRF apsauga saugo ir nuo XSS?
Atvirkščiai: XSS spraga išjungia bet kokią CSRF apsaugą, nes įterptas kodas puslapio vardu gali perskaityti galiojančius žetonus ir pridėti juos prie užklausos. Abi silpnybės šalinamos atskirai.