Di seguito i miei appunti (LLM-pimped) al fantastico video di Salvatore Sanfilippo:
Una vera e propria lezione sui Principi Fondamentali dell’Autenticazione. Non so perché, la sensazione che ho avuto seguendo il video è stata quella di “leggere” una vecchia zine hacker. Grazie antirez.
Il caso che ha acceso il dibattito
All’inizio del 2026, un utente di nome Sammy Azdoufal stava costruendo con Claude Code un controller personalizzato per il suo robot aspirapolvere DJI Romo, usando un joystick PS5. Quando la sua app si è connessa ai server DJI, un bug di autenticazione nel backend ha fatto sì che le sue credenziali venissero accettate come valide da circa 7.000 dispositivi attivi in oltre 20 paesi. Azdoufal aveva accesso a feed video in tempo reale, audio dai microfoni, planimetrie 2D delle abitazioni e posizioni geografiche dei dispositivi; tutto senza averlo cercato. DJI ha corretto la vulnerabilità il 24 febbraio 2026 e ha pagato 30.000 dollari di bug bounty. Fonte: The Verge
La causa radice era elementare: un session ID condiviso tra utenti diversi: una violazione basilare dei principi di autenticazione. Questo episodio è il punto di partenza ideale per ripassare quei principi che ogni sviluppatore dovrebbe conoscere.
Gestione delle password in fase di registrazione
Non imporre regole di complessità arbitrarie (maiuscole obbligatorie, simboli speciali, ecc.): mettono l’utente con le spalle al muro e lo spingono a riutilizzare la stessa password ovunque. È più utile:
- Richiedere una lunghezza minima (almeno 8 caratteri, meglio 10-12)
- Calcolare la “derivata prima” della password (differenza assoluta tra caratteri consecutivi) e verificare se è troppo comprimibile: sequenze come
abcdefo123456risulteranno brevissime dopo la compressione run-length e possono essere rifiutate o segnalate
Hashing delle password: mai cifrare, sempre hashare
Il termine corretto è hashing, non cifratura. La cifratura è un processo reversibile (simmetrico); l’hashing è una trasformazione non invertibile, e questo è esattamente ciò che si vuole per le password.
Non usare SHA-1, SHA-256 o algoritmi veloci: sono ottimizzati per la velocità e girano efficientemente su GPU, rendendo il brute-force economico. Occorre un algoritmo lento per design e resistente alle implementazioni hardware parallele.
Discussione interessante sul tema: BCrypt vs Argon2 and their hashing algorithms
| Algoritmo | Stato attuale | Resistenza GPU | Note |
|---|---|---|---|
| SHA-256/SHA-512 | ❌ Non usare per password | Bassa | Troppo veloce |
| bcrypt | ✅ Ancora valido | Media | Non più Gold Standard |
| Argon2id | ✅ Raccomandato | Alta | Vincitore PHC 2015 |
Argon2 è stato selezionato il 20 luglio 2015 come vincitore della Password Hashing Competition, una competizione aperta analoga a quelle NIST per AES e SHA-3. La sua caratteristica chiave è la memory-hardness: richiede per default 2 GB di RAM, rendendo gli attacchi con cluster GPU enormemente costosi.
⚠️ Attenzione alla migrazione: cambiare algoritmo di hashing in produzione è complesso. Le password esistenti non possono essere “ri-hashate” senza conoscere la password originale in chiaro. Scegliere bene fin dall’inizio, preferibilmente Argon2id.
Il ruolo del Salt
Il salt è una stringa casuale unica per ogni utente, generata tramite un Cryptographically Secure Pseudo-Random Number Generator (CSPRNG –> tipicamente /dev/random su Linux). Il salt viene passato all’algoritmo di hashing insieme alla password.
Senza salt, se due utenti hanno la stessa password, i loro hash nel database saranno identici: un attaccante può notarlo, dedurre che si tratta di una password comune, e concentrare il brute-force su quella.
Con salt diversi per ogni utente:
- La stessa password produce hash completamente diversi
- Un attacco a dizionario deve essere ripetuto separatamente per ogni utente
- È impossibile dedurre se più utenti condividono la stessa password
- Si evita la de-anonimizzazione di account secondari
import bcrypt
salt = bcrypt.gensalt(rounds=12) # 'rounds' è il parametro di costo
hashed = bcrypt.hashpw(b"mia_password", salt)
# Il salt e il costo sono inclusi nell'hash risultante
Il parametro rounds (o cost) controlla la lentezza: rounds=12 produce centinaia di millisecondi di latenza, cosa sufficiente a rendere il brute-force impraticabile, ma non così alto da saturare la CPU del server per ogni login.
Il flusso di autenticazione (login)
- L’utente invia la password in chiaro tramite HTTPS
- Il server recupera il salt associato all’utente nel database
- Si calcola l’hash della password ricevuta con quel salt
- Si confronta l’hash con quello memorizzato nel database
- Se coincidono → utente autenticato
Nota importante: se username o password sono errati, il messaggio di errore deve essere generico (“credenziali non valide”), senza distinguere tra “utente inesistente” e “password sbagliata”. Distinguerli creerebbe un oracle indiretto che permette di enumerare gli username validi.
Session ID: il cuore del bug DJI
Dopo l’autenticazione, si genera un session ID di almeno 128 bit tramite CSPRNG e lo si registra in un cookie lato client e nel database lato server. Questo è esattamente il punto in cui DJI ha fallito: il session ID era condiviso tra migliaia di utenti diversi.
Il cookie di sessione deve essere configurato con i flag corretti:
HttpOnly: non accessibile via JavaScript (protezione da XSS)Secure: trasmesso solo su HTTPSSameSite=Lax(oStrict): previene attacchi CSRF (richieste cross-site non autorizzate)
In caso di logout o reset password, il session ID va invalidato sia nel cookie che nel database (non basta cancellare il cookie lato client).
Protezione dal brute-force sul form di login
Un attaccante può tentare di indovinare le password direttamente sull’interfaccia di login. Le contromisure:
- Exponential backoff: dopo ogni tentativo fallito, il timeout prima del prossimo raddoppia (1s → 2s → 5s → 10s → 20s → …)
- Escalation progressiva: dopo N tentativi falliti, richiedere CAPTCHA o verifica via email
- Attenzione al DoS: bloccare un IP per ore può impedire all’utente legittimo di accedere — trovare il giusto equilibrio
Reset password e autenticazione a due fattori
Il reset della password va trattato come una procedura di autenticazione alternativa a sé stante. Se si usa l’email come canale di recupero, l’email diventa a tutti gli effetti un secondo fattore di autenticazione — trattarla con la stessa attenzione.
Per il 2FA, in ordine di sicurezza crescente:
- SMS: meno sicuro (vulnerabile a SIM swapping e social engineering sull’operatore), ma enormemente meglio di niente e comprensibile da tutti gli utenti
- TOTP (Google Authenticator, Authy, ecc.): più sicuro, adottato da una minoranza
- Token fisico (YubiKey, ecc.): il più sicuro, ma adottato da pochissimi
La raccomandazione è di offrire tutti e tre, incluso l’SMS: la maggior parte degli attaccanti non sarà in grado di aggirarlo, e abbassare la barriera all’adozione del 2FA vale più che offrire solo l’opzione più sicura che nessuno usa.
Generatori di numeri casuali crittografici (CSPRNG)
Salt, session ID e token di reset devono essere generati con un CSPRNG, non con Math.random() o equivalenti. Un CSPRNG:
- Raccoglie entropia da sorgenti hardware imprevedibili (jitter del clock, interrupt, movimenti del mouse, rumore analogico nei circuiti)
- Produce uno stream praticamente infinito di bit che, senza conoscere lo stato interno del generatore, è computazionalmente impossibile da predire o invertire
- Su Linux è esposto come
/dev/randomo/dev/urandom; i moderni processori x86 offrono anche l’istruzioneRDRANDcome sorgente hardware diretta
Riepilogo dei principi chiave
| Principio | Raccomandazione |
|---|---|
| Storage password | Argon2id (fallback: bcrypt con rounds ≥ 12) |
| Salt | Unico per utente, generato con CSPRNG |
| Session ID | ≥ 128 bit, CSPRNG, mai condiviso tra utenti |
| Cookie flags | HttpOnly, Secure, SameSite=Lax |
| Brute-force | Exponential backoff + escalation CAPTCHA/email |
| Messaggi di errore | Sempre generici (no oracle su username) |
| 2FA | Offrire SMS + TOTP + token fisico |
Immagine copertina: A Carload of Walnuts from California – Edward H. Mitchell (1909)