Proteggere le Operazioni Sensibili in PHP con CSRF Token

by theArchitect
SHARE
Proteggere le Operazioni Sensibili in PHP con CSRF Token
© Guida-HTML5.it

Introduzione

Quando sviluppiamo applicazioni web in PHP, spesso ci concentriamo su problemi molto noti come SQL injection o XSS. Tuttavia esiste un rischio altrettanto importante e molto comune: il Cross-Site Request Forgery, meglio conosciuto come CSRF.

Un attacco CSRF avviene quando un utente autenticato viene indotto a inviare una richiesta indesiderata al nostro sito. Il problema non nasce da una falla nel browser, ma dal fatto che il browser invia automaticamente cookie e sessioni verso il dominio corretto. Se l’applicazione non verifica l’intenzione reale dell’utente, un sito esterno può sfruttare la sessione già attiva per compiere azioni come cambiare email, modificare impostazioni o confermare operazioni.

In questo tutorial vedremo un approccio pratico e molto usato in PHP: la protezione delle richieste sensibili tramite token CSRF. È una tecnica semplice, efficace e adatta a molte applicazioni tradizionali basate su form HTML.

Codice completo

Nel seguente esempio realizziamo una piccola pagina PHP che mostra un form per aggiornare il profilo utente. Il form include un token CSRF salvato in sessione e verificato lato server prima di eseguire l’operazione.

<?php
declare(strict_types=1);

session_start();

/**
 * Genera un token CSRF se non esiste già.
 * Il token viene memorizzato in sessione e inserito nei form.
 */
function getCsrfToken(): string
{
    if (empty($_SESSION[´csrf_token´])) {
        $_SESSION[´csrf_token´] = bin2hex(random_bytes(32));
    }

    return $_SESSION[´csrf_token´];
}

/**
 * Verifica il token CSRF ricevuto dal form.
 */
function validateCsrfToken(?string $token): bool
{
    if (!isset($_SESSION[´csrf_token´]) || $token === null) {
        return false;
    }

    return hash_equals($_SESSION[´csrf_token´], $token);
}

$message = ´´;

if ($_SERVER[´REQUEST_METHOD´] === ´POST´) {
    $csrfToken = $_POST[´csrf_token´] ?? null;

    if (!validateCsrfToken($csrfToken)) {
        http_response_code(403);
        $message = ´Richiesta non valida: token CSRF mancante o errato.´;
    } else {
        // Esempio di operazione sensibile: aggiornamento profilo
        $name = trim($_POST[´name´] ?? ´´);
        $email = trim($_POST[´email´] ?? ´´);

        if ($name === ´´ || $email === ´´) {
            $message = ´Compila tutti i campi.´;
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $message = ´Email non valida.´;
        } else {
            // Qui normalmente aggiorneresti il database
            $message = ´Profilo aggiornato correttamente per ´ . htmlspecialchars($name, ENT_QUOTES, ´UTF-8´);
        }

        // Rigenera il token dopo ogni POST valido per ridurre il rischio di riuso
        unset($_SESSION[´csrf_token´]);
    }
}

$token = getCsrfToken();
?>

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Esempio CSRF Token</title>
</head>
<body>
    <h1>Aggiorna profilo</h1>

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

    <form method="post" action="">
        <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token, ENT_QUOTES, ´UTF-8´) ?>">

        <p>
            <label for="name">Nome:</label><br>
            <input type="text" id="name" name="name" required>
        </p>

        <p>
            <label for="email">Email:</label><br>
            <input type="email" id="email" name="email" required>
        </p>

        <p>
            <button type="submit">Salva modifiche</button>
        </p>
    </form>
</body>
</html>

Spiegazione

La logica è composta da pochi passaggi fondamentali:

1. Creazione del token

La funzione getCsrfToken() genera un valore casuale con random_bytes(32) e lo converte in stringa esadecimale con bin2hex(). Il token viene salvato in $_SESSION, così il server può confrontarlo in seguito con quello inviato dal form.

2. Inserimento nel form

Il token viene aggiunto come input nascosto:

<input type="hidden" name="csrf_token" ...>

In questo modo il browser lo invia insieme agli altri campi del form. Un sito esterno non può conoscerlo facilmente, quindi non riuscirà a costruire una richiesta valida.

3. Verifica lato server

Quando arriva una richiesta POST, il server legge il token con:

$csrfToken = $_POST[´csrf_token´] ?? null;

La funzione validateCsrfToken() confronta il valore ricevuto con quello memorizzato in sessione usando hash_equals(). Questo dettaglio è importante: evita problemi di timing attack e rende il confronto più sicuro rispetto a un semplice ==.

4. Blocco delle richieste non valide

Se il token manca o è errato, il codice restituisce 403 Forbidden. Questo significa che l’operazione non viene eseguita. È una barriera essenziale per proteggere richieste come aggiornamenti profilo, cambio password, eliminazione account o conferma ordini.

5. Rigenerazione del token

Dopo una richiesta POST valida, il token viene eliminato con unset($_SESSION[´csrf_token´]). Alla richiesta successiva verrà generato un nuovo token. Questa pratica riduce il rischio di riutilizzo del token e migliora la sicurezza complessiva.

Best practice

  • Proteggi tutte le operazioni che modificano lo stato: POST, PUT, PATCH e DELETE devono essere difese, non solo i form classici.
  • Usa token unici e casuali: genera i token con random_bytes(), non con funzioni prevedibili come mt_rand().
  • Confronta i token con hash_equals(): è la scelta corretta per confronti sicuri.
  • Valida sempre lato server: un token presente nel form non basta; la verifica deve avvenire nel backend.
  • Rigenera il token dopo operazioni importanti: è utile per ridurre il rischio di replay.
  • Non affidarti solo al referer: l’header Referer può mancare o essere alterato, quindi non è una protezione affidabile da solo.
  • Usa cookie con SameSite: impostare SameSite=Lax o SameSite=Strict aiuta a ridurre ulteriormente la superficie d’attacco, ma non sostituisce il token CSRF.
  • Proteggi anche le richieste AJAX: se usi JavaScript, invia il token in un header personalizzato o nel body della richiesta.
  • Non esporre il token inutilmente: inseriscilo solo dove serve, cioè nei form o nelle richieste autorizzate.

Riepilogo

La protezione CSRF è una misura fondamentale in qualsiasi applicazione PHP che gestisce utenti autenticati e operazioni sensibili. Il principio è semplice: il server deve verificare che la richiesta provenga davvero dalla tua applicazione e non da un sito esterno che sfrutta una sessione già attiva.

La soluzione più efficace e pratica consiste nell’usare un token CSRF generato casualmente, salvato in sessione e confrontato a ogni richiesta che modifica dati o stato. Con poche righe di codice puoi aumentare in modo significativo la sicurezza del tuo progetto.

Se vuoi ricordare una sola regola, tieni questa: ogni azione che cambia dati deve essere verificata con un controllo server-side affidabile. In PHP, il token CSRF è uno degli strumenti più semplici e solidi per farlo.

Approfondisci con risorse ufficiali

  • PHP Manual - session_start(): documentazione ufficiale sulla gestione delle sessioni.
  • PHP Manual - random_bytes(): per generare token casuali sicuri.
  • PHP Manual - hash_equals(): per confronti sicuri tra stringhe.
  • OWASP CSRF Prevention Cheat Sheet: guida pratica e autorevole sulle tecniche di difesa contro CSRF.
  • MDN Web Docs - SameSite cookies: approfondimento sul comportamento dei cookie nei browser moderni.

SHARE