Introduzione alla Sicurezza delle Password
Uno degli errori più gravi che uno sviluppatore web può commettere è memorizzare le password degli utenti in chiaro nel database. Se un database venisse compromesso tramite un attacco di SQL Injection o un leak di backup, ogni account sarebbe immediatamente accessibile agli hacker.
Per prevenire questo scenario, utilizziamo il hashing. A differenza della crittografia (che è bidirezionale e permette di tornare al testo originale tramite una chiave), l'hashing è un processo unidirezionale. Una volta che una password è stata trasformata in un hash, non è possibile "decriptarla" per ottenere la password originale. L'unico modo per verificare l'identità di un utente è hashare la password inserita durante il login e confrontare il risultato con l'hash salvato nel database.
Perché non usare MD5 o SHA-1?
In passato, molti sviluppatori utilizzavano funzioni come md5() o sha1(). Oggi, queste funzioni sono considerate totalmente insicure per la gestione delle password per due motivi principali:
- Velocità di calcolo: MD5 e SHA-1 sono progettati per essere estremamente veloci (utili per verificare l'integrità di file di grandi dimensioni). Questo è un vantaggio per il sistema, ma un disastro per la sicurezza: un computer moderno può generare miliardi di hash MD5 al secondo, rendendo gli attacchi di brute-force e l'uso di Rainbow Tables (tabelloni di hash pre-calcolati) incredibilmente efficaci.
- Collisioni: Entrambi gli algoritmi soffrono di collisioni, ovvero situazioni in cui due input diversi producono lo stesso hash, rendendo il sistema vulnerabile.
L'approccio moderno: password_hash()
PHP ha introdotto un set di funzioni integrate che astraggono la complessità della gestione della sicurezza. La funzione principale è password_hash(). Questa funzione gestisce automaticamente due aspetti cruciali: l'algoritmo di hashing e il salt.
Cos'è il Salt?
Il salt è una stringa casuale aggiunta alla password prima dell'hashing. Senza il salt, due utenti con la stessa password ("123456") avrebbero lo stesso hash nel database. Un attaccante potrebbe così identificare tutti gli utenti con password comuni. Il salt rende ogni hash unico, anche se la password è identica per più utenti.
Ecco come implementare correttamente la creazione di un hash in PHP:
<?php
// La password fornita dall'utente tramite un form
$passwordUtente = "PasswordSicura123!";
// Utilizziamo password_hash()
// PASSWORD_DEFAULT indica a PHP di usare l'algoritmo più sicuro disponibile al momento (attualmente bcrypt)
// Il salt viene generato automaticamente e incluso nella stringa risultante
$hashSicuro = password_hash($passwordUtente, PASSWORD_DEFAULT);
echo "Password originale: $passwordUtente\
";
echo "Hash generato: $hashSicuro\
";
// Nota: L'hash risultante contiene l'algoritmo, il costo e il salt,
// quindi non serve salvare il salt in una colonna separata del database.
?>
Nel codice sopra, PASSWORD_DEFAULT è la scelta consigliata. Se in futuro PHP aggiornasse l'algoritmo predefinito (ad esempio passando da bcrypt ad Argon2), il tuo codice rimarrebbe compatibile.
Verifica delle Password con password_verify()
Poiché l'hashing è unidirezionale, non possiamo fare un semplice confronto if ($input == $hash). Dobbiamo usare la funzione password_verify(), che estrae il salt e l'algoritmo dall'hash salvato e li applica alla password inserita dall'utente.
<?php
// Simuliamo un dato recuperato dal database
$hashSalvatoNelDb = '$2y$10$nOUIsS7B.Wp.XqK.GqYVbeS1.p9A8fV6uX6z9kI9S1P2q3r4s5t6u';
$passwordInserita = "PasswordSicura123!";
if (password_verify($passwordInserita, $hashSalvatoNelDb)) {
echo "Accesso consentito! La password è corretta.";
} else {
echo "Accesso negato! Password errata.";
}
?>
Questa funzione è resistente agli attacchi di timing (timing attacks), ovvero non rivela quanto tempo impiega per fallire, impedendo a un attaccante di dedurre informazioni sulla struttura dell'hash.
Approfondimento: Bcrypt vs Argon2
Sebbene PASSWORD_DEFAULT sia l'opzione più semplice, PHP supporta algoritmi specifici che offrono diversi livelli di protezione:
Bcrypt (PASSWORD_BCRYPT)
È lo standard de facto. La sua forza risiede nel parametro "cost", che determina quante iterazioni di hashing devono essere effettuate. Aumentando il costo, l'hashing diventa più lento, rendendo i tentativi di brute-force proibitivi per l'attaccante.
Argon2 (PASSWORD_ARGON2I e PASSWORD_ARGON2ID)
Vincitore della Password Hashing Competition, Argon2 è più moderno di Bcrypt. Non solo è computazionalmente costoso, ma è progettato per essere resistente agli attacchi basati su GPU e FPGA, poiché permette di configurare non solo il tempo di calcolo, ma anche la quantità di memoria RAM richiesta per generare l'hash.
Esempi Pratici: Implementazione in un Sistema di Login
Vediamo come integrare queste funzioni in un flusso di lavoro reale, dalla registrazione al login.
1. Fase di Registrazione
<?php
// Connessione al DB (ipotetica)
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $_POST['username'];
$pass = $_POST['password'];
// Hashing della password prima dell'inserimento
$hashedPassword = password_hash($pass, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
if ($stmt->execute([$user, $hashedPassword])) {
echo "Utente registrato con successo!";
}
}
?>
2. Fase di Login
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $_POST['username'];
$pass = $_POST['password'];
// Recuperiamo l'hash dal DB basandoci sullo username
$stmt = $pdo->prepare("SELECT password FROM users WHERE username = ?");
$stmt->execute([$user]);
$userRow = $stmt->fetch();
if ($userRow && password_verify($pass, $userRow['password'])) {
session_start();
$_SESSION['user_id'] = $userRow['id'];
echo "Benvenuto nel sistema!";
} else {
echo "Credenziali non valide.";
}
}
?>
Errori Comuni e FAQ
"Ho aggiunto un mio salt manuale, è meglio?"
No. Un errore comune è concatenare una stringa fissa (es. password_hash($pass . "MioSaltSegreto", PASSWORD_DEFAULT)). Questo non aggiunge sicurezza reale e, se la stringa segreta venisse compromessa, torneresti alla situazione di base. password_hash() genera un salt unico per ogni password e lo memorizza nell'hash stesso; è l'unico modo corretto di procedere.
"Quale lunghezza deve avere la colonna password nel database?"
Se usi Bcrypt, l'hash è di 60 caratteri. Tuttavia, è fortemente consigliato usare un campo VARCHAR(255). Questo perché se in futuro deciderai di passare a Argon2 o se PHP aggiornerà l'algoritmo predefinito, gli hash potrebbero diventare più lunghi. Evita di limitare il campo a 60 caratteri per non rischiare di troncare l'hash, rendendolo impossibile da verificare.
"Come aggiorno gli hash se cambio l'algoritmo o il costo?"
Puoi usare la funzione password_needs_rehash(). Questa funzione controlla se l'hash esistente è stato creato con l'algoritmo corrente o con i parametri di costo desiderati.
if (password_needs_rehash($hashSalvato, PASSWORD_DEFAULT, ['cost' => 12])) {
// L'hash è obsoleto, ricalcolalo usando la password in chiaro fornita durante il login
$nuovoHash = password_hash($passwordInserita, PASSWORD_DEFAULT, ['cost' => 12]);
// Aggiorna il database con $nuovoHash
}
Prossimi Passi e Approfondimenti
Ora che hai implementato un sistema di hashing sicuro, la sicurezza non finisce qui. Per rendere l'applicazione veramente robusta, considera i seguenti passi:
- Implementazione del MFA (Multi-Factor Authentication): Anche un hash perfetto non protegge l'utente se questo usa una password banale. L'aggiunta di un codice via email o app (TOTP) è essenziale.
- Rate Limiting: Implementa un sistema per bloccare temporaneamente l'IP di un utente dopo 5 tentativi di login falliti per prevenire attacchi di brute-force.
- Politiche di Password: Imponi requisiti minimi di lunghezza e complessità per evitare che gli utenti scelgano password facilmente indovinabili.
- Studio di HTTPS: Assicurati che l'intera applicazione giri su HTTPS, altrimenti la password viaggia in chiaro tra il browser e il server, rendendo inutile l'hashing a valle.