Proteggere le sessioni PHP dagli attacchi CSRF

by theArchitect
SHARE
Proteggere le sessioni PHP dagli attacchi CSRF
© Guida-HTML5.it

Introduzione

Quando un utente effettua il login, PHP associa la sua identità a una sessione. Da quel momento, ogni richiesta che include il cookie di sessione può essere interpretata dall’applicazione come proveniente dall’utente autenticato.

Questa comodità introduce però un rischio: un attaccante può creare una pagina esterna che invia automaticamente una richiesta verso il nostro sito. Il browser dell’utente, se già autenticato, potrebbe allegare automaticamente il cookie di sessione. Questo tipo di attacco è chiamato CSRF, cioè Cross-Site Request Forgery.

Un esempio pratico è un form per cambiare l’indirizzo email. Se l’applicazione accetta una richiesta POST senza verificare che sia stata generata dal proprio sito, una pagina malevola potrebbe tentare di inviare la modifica al posto dell’utente.

La difesa più comune consiste nell’utilizzare un token CSRF: un valore casuale memorizzato nella sessione e inserito anche nel form HTML. Al momento dell’invio, il server confronta il token ricevuto con quello presente nella sessione.

Codice completo

Il seguente esempio mostra un flusso completo per proteggere un form autenticato. Il file può essere chiamato profilo.php. L’esempio presuppone che il login sia già stato completato e che nella sessione sia presente user_id.

<?php
declare(strict_types=1);

session_start();

/*
 * In un´applicazione reale, questa condizione dovrebbe essere
 * sostituita da un controllo centralizzato dell´utente autenticato.
 */
if (!isset($_SESSION[´user_id´])) {
    http_response_code(401);
    exit(´Accesso negato: devi effettuare il login.´);
}

/*
 * Crea il token soltanto se non esiste già.
 * random_bytes() genera dati casuali critograficamente sicuri.
 */
if (empty($_SESSION[´csrf_token´])) {
    $_SESSION[´csrf_token´] = bin2hex(random_bytes(32));
}

/*
 * Verifica il form prima di elaborare i dati.
 */
if ($_SERVER[´REQUEST_METHOD´] === ´POST´) {
    $tokenRicevuto = $_POST[´csrf_token´] ?? ´´;
    $tokenSessione = $_SESSION[´csrf_token´];

    /*
     * hash_equals() evita confronti vulnerabili a timing attack.
     */
    if (
        !is_string($tokenRicevuto) ||
        !hash_equals($tokenSessione, $tokenRicevuto)
    ) {
        http_response_code(403);
        exit(´Richiesta non valida: token CSRF errato.´);
    }

    $email = filter_input(
        INPUT_POST,
        ´email´,
        FILTER_VALIDATE_EMAIL
    );

    if ($email === false || $email === null) {
        $messaggio = ´Inserisci un indirizzo email valido.´;
    } else {
        /*
         * Qui si dovrebbe aggiornare il database con una query preparata.
         */
        $messaggio = ´Profilo aggiornato correttamente.´;
    }
}
?>

<?php if (isset($messaggio)): ?>
    <p><?= htmlspecialchars($messaggio, ENT_QUOTES, ´UTF-8´) ?></p>
<?php endif; ?>

<form method="post" action="profilo.php">
    <label for="email">Nuova email</label>
    <input type="email" id="email" name="email" required>

    <input
        type="hidden"
        name="csrf_token"
        value="<?= htmlspecialchars(
            $_SESSION[´csrf_token´],
            ENT_QUOTES,
            ´UTF-8´
        ) ?>"
    >

    <button type="submit">Salva modifiche</button>
</form>

Spiegazione

Creazione del token

random_bytes(32) produce 32 byte casuali. Con bin2hex() questi byte vengono convertiti in una stringa esadecimale facilmente utilizzabile in un campo HTML. Il token viene salvato in $_SESSION[´csrf_token´], quindi è associato alla sessione dell’utente corrente.

Il token non deve essere prevedibile. Non bisogna utilizzare valori come l’ID dell’utente, l’email, la data corrente o stringhe generate con rand(). Questi dati non offrono sufficiente casualità per un controllo di sicurezza.

Inserimento nel form

Il token viene inserito in un campo nascosto. Il browser lo invia insieme agli altri dati del form, ma una pagina appartenente a un dominio esterno non dovrebbe poter leggere il token dalla sessione del nostro sito a causa della Same-Origin Policy.

Il campo nascosto non è una protezione da solo: il suo valore deve sempre essere verificato lato server. Tutti i controlli di sicurezza realmente importanti devono essere eseguiti sul server, mai affidati esclusivamente a JavaScript.

Verifica con hash_equals()

hash_equals() confronta due stringhe in modo più sicuro rispetto a un semplice operatore ===, riducendo il rischio di attacchi basati sul tempo di confronto. Se il token è mancante o non corrisponde, il server risponde con codice HTTP 403 e non modifica alcun dato.

Best practice

  • Proteggi con un token CSRF tutte le operazioni che modificano dati: POST, PUT, PATCH e DELETE.
  • Non usare token CSRF prevedibili o condivisi tra tutti gli utenti.
  • Genera il token con random_bytes(), non con rand() o funzioni basate sull’orologio.
  • Confronta i token con hash_equals().
  • Applica il controllo CSRF anche a operazioni come cambio password, modifica email, eliminazione account e azioni amministrative.
  • Usa richieste POST per le operazioni che modificano lo stato. Le richieste GET dovrebbero essere idempotenti e prive di effetti collaterali.
  • Imposta il cookie di sessione con gli attributi Secure, HttpOnly e SameSite=Lax o Strict, quando compatibile con l’applicazione.
  • Non mostrare il token nei log, negli URL o nei messaggi di errore.
  • Per applicazioni con molti form, centralizza la logica in funzioni riutilizzabili, evitando controlli copiati manualmente.
  • Valida anche i dati ricevuti: il token CSRF dimostra l’origine della richiesta, ma non sostituisce la validazione dell’email o delle altre informazioni.

Un’alternativa utile per API e applicazioni JavaScript è inviare il token in un header personalizzato, ad esempio X-CSRF-Token. Il server deve comunque recuperarlo dalla sessione e confrontarlo con hash_equals().

Riepilogo

La sessione identifica l’utente, ma non garantisce che ogni richiesta sia stata realmente intenzionata dall’utente. Un attacco CSRF sfrutta proprio questo comportamento del browser.

La soluzione consiste nel generare un token casuale, conservarlo nella sessione, inserirlo nei form e verificarlo sul server prima di eseguire qualsiasi modifica. Insieme a cookie configurati correttamente, richieste HTTP appropriate e validazione dei dati, il token CSRF costituisce una difesa essenziale per le applicazioni PHP con autenticazione tramite sessione.

Approfondisci con risorse ufficiali

SHARE