In breve: il CSRF in PHP si previene facendo in modo che ogni richiesta che modifica qualcosa debba portare con sé un token segreto, legato alla sessione, che si verifica lato server con hash_equals(). In aggiunta si controllano gli header Origin o Sec-Fetch-Site e si impostano i cookie di sessione con SameSite. Le richieste GET non devono mai modificare dati, e anche il modulo di login richiede protezione.
Un clic che nessuno nota
Una collaboratrice è connessa al backend dello shop e in una seconda scheda apre un sito estraneo. Questo sito invia in background un modulo al backend, ad esempio per cambiare un indirizzo e-mail o creare un account. Il browser allega automaticamente il cookie di sessione. Per l'applicazione la richiesta appare come qualsiasi altra proveniente dal proprio team.
Questo è il Cross-Site Request Forgery, in breve CSRF: una richiesta che un utente connesso invia a sua insaputa a un'applicazione di cui si fida. L'aggressore non vede la risposta, ma non gli serve. Basta che l'azione venga eseguita.
Il CSRF non finisce nei titoli come PHP2Shell, ma nelle applicazioni PHP sviluppate in proprio è un rilievo frequente. Framework come Symfony e Laravel offrono una protezione; le vulnerabilità nascono lì soprattutto dove è stata disattivata per singole route. Gli sviluppi propri senza framework spesso non ne hanno affatto.
CSRF in PHP: quali richieste vanno protette
Hanno bisogno di protezione tutte le richieste che modificano uno stato: salvare, cancellare, ordinare, assegnare permessi, cambiare impostazioni, caricare file, disconnettersi. Le chiamate in lettura non richiedono alcun token, finché leggono davvero soltanto.
Ne discende la prima regola: nessuna modifica via GET. Link come loeschen.php?id=… si possono innescare con particolare facilità da siti esterni, ad esempio tramite un'immagine incorporata. Le azioni che modificano appartengono a POST, PUT, PATCH o DELETE.
La seconda regola: proteggere anche il login. Nel cosiddetto login CSRF una vittima viene fatta accedere a sua insaputa a un account dell'aggressore. Se vi inserisce dati di pagamento o documenti, questi finiscono all'aggressore. Un token nel modulo di login e un nuovo ID di sessione dopo l'accesso riuscito lo impediscono.
Il modello Synchronizer Token in PHP
Per le applicazioni classiche con sessioni OWASP raccomanda il modello Synchronizer Token: il server genera un token casuale, lo salva nella sessione e se lo aspetta di ritorno in ogni modulo che modifica dati. È importante che il token venga inviato nel modulo o in un header apposito, non in un 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') ?>">
<!-- altri campi -->
</form>
hash_equals() confronta in tempo costante, così il tempo di risposta non rivela nulla sul valore corretto. Dopo il login dovrebbe rinnovare l'ID di sessione con session_regenerate_id(true) e generare un nuovo token. Perché random_bytes() sia la fonte giusta e quali funzioni siano inadatte lo spiega Numeri casuali e token sicuri in PHP.
Un token per sessione basta per la maggior parte delle applicazioni. Token per modulo o per richiesta aumentano un po' la protezione, ma causano problemi con più schede aperte e con il pulsante Indietro. Più importante della frequenza del cambio è che il token non compaia mai negli URL, dove può finire nei log, nella cronologia del browser e nelle indicazioni di referrer verso siti esterni.
Per le applicazioni senza stato e senza sessione OWASP indica come alternativa il double-submit cookie firmato: un token legato alla sessione tramite HMAC. La variante semplice, non firmata, non è espressamente raccomandata.
Secondo livello: Origin, Sec-Fetch-Site e SameSite
I browser moderni inviano con le richieste informazioni sulla propria origine. L'header Origin indica la pagina chiamante, Sec-Fetch-Site dice se la richiesta proviene dalla stessa pagina (same-origin), da una correlata (same-site) o da una esterna (cross-site). Se ne può ricavare una semplice verifica aggiuntiva:
<?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 sottolinea due punti: l'origine va confrontata per intero, perché domini dal suono simile non passino, e un header mancante o il valore null non devono valere come origine propria. Poiché i browser più vecchi non sempre inviano gli header, il token resta la difesa principale.
Il terzo livello sono i cookie di sessione con SameSite. PHP supporta session.cookie_samesite dalla versione 7.3, il valore predefinito è vuoto. Imposti almeno Lax; per i backend puri spesso è possibile Strict. Le altre impostazioni dei cookie le descrive la guida Mettere in sicurezza le sessioni in PHP.
Se usa un CMS, di solito porta con sé strumenti propri. WordPress, ad esempio, lavora con i cosiddetti nonce, che i plugin integrano con funzioni come wp_nonce_field() e check_admin_referer(). Nella pratica le lacune nascono soprattutto nelle estensioni che dimenticano tali verifiche in singole azioni. Controlli quindi con particolare attenzione i plugin scritti in proprio o poco diffusi.
Per le interfacce usate dai browser da altri domini decide inoltre la configurazione CORS. Se il server restituisce ogni origine richiedente come consentita e ammette al contempo i cookie, neutralizza lui stesso la protezione del browser. OWASP raccomanda di aprire CORS solo per origini esplicitamente affidabili; un'autorizzazione generalizzata di tutti i sottodomini diventa un problema non appena uno di essi finisce in mani estranee.
Checklist per le applicazioni esistenti
Nei progetti cresciuti nel tempo aiuta una revisione sistematica:
- Rilevare tutte le route e gli script che modificano dati, anche endpoint Ajax e upload di file.
- Verificare se per essi si usa GET e passare a POST o ai metodi adatti.
- Per ogni route che modifica dati dimostrare che un token viene verificato; cercare in modo mirato le eccezioni nel framework.
- Testare separatamente login, logout e cambio della password.
- Aggiungere
session.cookie_samesitee la verifica dell'origine. - Eliminare le vulnerabilità XSS, perché il codice iniettato può leggere ogni token. Il procedimento lo descrive Prevenire il cross-site scripting in PHP.
Una protezione CSRF verificata solo nel frontend con JavaScript è inefficace. La verifica deve avvenire sul server, prima che l'azione venga eseguita.
Come aiuta concretamente PHP SOS?
- Rileviamo tutte le route della sua applicazione PHP che modificano dati e verifichiamo dove un token manca o è stato disattivato.
- Inseriamo un punto di verifica centrale, invece di distribuire le verifiche dei token su molti script.
- Aggiungiamo verifica dell'origine e impostazioni SameSite, senza disturbare i flussi di pagamento o di login.
- Dopo il passaggio ripetiamo i test e documentiamo i risultati.
Una verifica completa della sua applicazione è descritta nel servizio audit di sicurezza e penetration test.
Domande frequenti
SameSite=Lax basta come protezione CSRF?
No. Secondo OWASP i cookie SameSite sono un livello aggiuntivo, non una sostituzione. Lax continua a inviare i cookie nelle normali visualizzazioni di pagina via GET, e gli attacchi da sottodomini dello stesso sito non sono coperti.
Le API JSON hanno bisogno di un token CSRF?
Se l'API si autentica con i cookie, sì, oppure serve una verifica equivalente. Un'alternativa sono header di richiesta propri, che i browser consentono da siti esterni solo dopo una verifica preliminare CORS. In tal caso però l'autorizzazione CORS deve includere solo origini esplicitamente affidabili.
Va protetto anche il modulo di login?
Sì. Nel login CSRF una vittima viene fatta accedere a sua insaputa a un account altrui e vi inserisce i propri dati. Un token nel modulo di login e una nuova sessione dopo l'accesso lo impediscono.
La protezione CSRF protegge anche dall'XSS?
È il contrario: una vulnerabilità XSS neutralizza qualsiasi protezione CSRF, perché il codice iniettato può leggere token validi e inviarli insieme alla richiesta a nome della pagina. Le due debolezze vanno risolte separatamente.