Come aggiungere correttamente il token CSRF (Cross-Site Request Forgery) utilizzando PHP


96

Sto cercando di aggiungere un po 'di sicurezza ai moduli sul mio sito web. Uno dei moduli utilizza AJAX e l'altro è un semplice modulo "contattaci". Sto cercando di aggiungere un token CSRF. Il problema che ho è che il token viene visualizzato solo nel "valore" HTML alcune volte. Il resto del tempo il valore è vuoto. Ecco il codice che sto usando nel modulo AJAX:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

Eventuali suggerimenti?


Solo curioso, a cosa token_timeserve?
zerkms

@zerkms attualmente non sto utilizzando token_time. Stavo per limitare il tempo entro il quale un token è valido, ma non ho ancora implementato completamente il codice. Per motivi di chiarezza, l'ho rimosso dalla domanda sopra.
Ken

1
@Ken: così l'utente può ottenere il caso quando ha aperto un modulo, pubblicarlo e ottenere un token non valido? (poiché è stato invalidato)
zerkms

@zerkms: Grazie, ma sono un po 'confuso. C'è qualche possibilità che potresti fornirmi un esempio?
Ken

2
@Ken: certo. Supponiamo che il token scada alle 10:00. Adesso sono le 09:59. L'utente apre un modulo e riceve un token (che è ancora valido). Quindi l'utente compila il modulo per 2 minuti e lo invia. Finché sono le 10:01 ora - il token viene considerato non valido, quindi l'utente riceve un errore del modulo.
zerkms

Risposte:


286

Per il codice di sicurezza, non generare i token in questo modo: $token = md5(uniqid(rand(), TRUE));

Prova questo:

Generazione di un token CSRF

PHP 7

session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

Nota a margine: uno dei progetti open source del mio datore di lavoro è un'iniziativa di backport random_bytes()e random_int()in progetti PHP 5. È concesso in licenza dal MIT e disponibile su Github e Composer come paragonie / random_compat .

PHP 5.3+ (o con ext-mcrypt)

session_start();
if (empty($_SESSION['token'])) {
    if (function_exists('mcrypt_create_iv')) {
        $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
    }
}
$token = $_SESSION['token'];

Verifica del token CSRF

Non limitarti a usare ==o anche a ===usare hash_equals()(solo PHP 5.6+, ma disponibile per le versioni precedenti con la libreria hash-compat ).

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

Andare oltre con i token Per-Form

Puoi limitare ulteriormente i token in modo che siano disponibili solo per un particolare modulo utilizzando hash_hmac(). HMAC è una particolare funzione hash con chiave che è sicura da usare, anche con funzioni hash più deboli (ad esempio MD5). Tuttavia, consiglio di utilizzare invece la famiglia di funzioni hash SHA-2.

Innanzitutto, genera un secondo token da utilizzare come chiave HMAC, quindi usa la logica come questa per renderlo:

<input type="hidden" name="token" value="<?php
    echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />

E quindi utilizzando un'operazione congruente durante la verifica del token:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

I token generati per un modulo non possono essere riutilizzati in un altro contesto senza saperlo $_SESSION['second_token']. È importante utilizzare un token separato come chiave HMAC rispetto a quello appena rilasciato nella pagina.

Bonus: approccio ibrido + integrazione ramoscello

Chiunque utilizzi il motore di modelli Twig può beneficiare di una doppia strategia semplificata aggiungendo questo filtro al proprio ambiente Twig:

$twigEnv->addFunction(
    new \Twig_SimpleFunction(
        'form_token',
        function($lock_to = null) {
            if (empty($_SESSION['token'])) {
                $_SESSION['token'] = bin2hex(random_bytes(32));
            }
            if (empty($_SESSION['token2'])) {
                $_SESSION['token2'] = random_bytes(32);
            }
            if (empty($lock_to)) {
                return $_SESSION['token'];
            }
            return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
        }
    )
);

Con questa funzione Twig, puoi utilizzare entrambi i token generici in questo modo:

<input type="hidden" name="token" value="{{ form_token() }}" />

O la variante bloccata:

<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />

Twig si occupa solo del rendering dei modelli; è comunque necessario convalidare correttamente i token. A mio parere, la strategia Twig offre maggiore flessibilità e semplicità, pur mantenendo la possibilità per la massima sicurezza.


Token CSRF monouso

Se hai un requisito di sicurezza per cui ogni token CSRF può essere utilizzato esattamente una volta, la strategia più semplice lo rigenera dopo ogni convalida riuscita. Tuttavia, ciò invaliderà ogni token precedente che non si mescola bene con le persone che navigano su più schede contemporaneamente.

Paragon Initiative Enterprises mantiene una libreria Anti-CSRF per questi casi d'angolo. Funziona esclusivamente con gettoni per modulo monouso. Quando è memorizzato un numero sufficiente di token nei dati della sessione (configurazione predefinita: 65535), verranno eliminati per primi i token non riscattati più vecchi.


bello, ma come cambiare il token $ dopo che l'utente ha inviato il modulo? nel tuo caso, un token utilizzato per la sessione utente.
Akam

1
Guarda attentamente come viene implementato github.com/paragonie/anti-csrf . I gettoni sono monouso, ma memorizza più.
Scott Arciszewski

@ScottArciszewski Cosa ne pensi di generare un messaggio digest dall'id di sessione con un segreto e quindi confrontare il digest del token CSRF ricevuto con l'hashing di nuovo dell'id di sessione con il mio segreto precedente? Spero tu capisca quello che intendo.
MNR

1
Ho una domanda sulla verifica del token CSRF. Se $ _POST ['token'] è vuoto, non dovremmo procedere, perché la richiesta di questo post è stata inviata senza il token, giusto?
Hiroki

1
Perché verrà riprodotto nel modulo HTML e vuoi che sia imprevedibile in modo che gli aggressori non possano semplicemente falsificarlo. Stai davvero implementando l'autenticazione challenge-response qui, non semplicemente "sì, questo modulo è legittimo" perché un utente malintenzionato può semplicemente falsificarlo.
Scott Arciszewski

24

Avviso di sicurezza : md5(uniqid(rand(), TRUE))non è un modo sicuro per generare numeri casuali. Vedi questa risposta per ulteriori informazioni e una soluzione che sfrutta un generatore di numeri casuali crittograficamente sicuro.

Sembra che tu abbia bisogno di un altro con il tuo if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}

11
Nota: non mi fiderei md5(uniqid(rand(), TRUE));per i contesti di sicurezza.
Scott Arciszewski

2

La variabile $tokennon viene recuperata dalla sessione quando è presente

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.