Gestione delle sessioni sicure in PHP: session fixation, hijacking e best practice
Il 11 aprile 2025 sono stato ingaggiato come consulente offensive-security da una banca italiana di dimensione media - circa 140 filiali distribuite nel Centro-Nord, patrimonio gestito di 4,2 miliardi di euro - per un'attività di penetration testing focalizzato sul loro portale di home banking e sull'area riservata corporate. L'ingaggio era stato richiesto dal CRO della banca come parte di un audit annuale di conformità e rappresentava un'iniziativa proattiva, non una risposta a un incidente specifico. L'applicazione era un portale PHP custom sviluppato internamente dieci anni prima, progressivamente modernizzato ma con il core di gestione sessione e autenticazione rimasto sostanzialmente immutato rispetto al design originario. Il team di sviluppo interno era composto da 14 persone con processi di code review strutturati e un security officer dedicato - insomma, non un'organizzazione sprovveduta dal punto di vista della postura di sicurezza.
Nelle prime due giornate dell'assessment ho dimostrato una session fixation sfruttabile in produzione in meno di 10 minuti di analisi, e nelle tre giornate successive ho identificato tre ulteriori vulnerabilità critiche legate alla gestione delle sessioni - due problemi di session hijacking attraverso meccanismi paralleli, e una privilege escalation post-login abilitata da un pattern errato di rigenerazione dell'ID di sessione. La cosa che ha colpito il team interno della banca durante la sessione di debrief è stata la banalità tecnica di queste vulnerabilità: non richiedevano exploit sofisticati, né competenze di reverse engineering avanzate, né accesso privilegiato al sistema. Richiedevano solo l'applicazione meccanica di tecniche documentate da oltre dieci anni nel materiale pubblico OWASP. Eppure tre sviluppatori senior con processi di review e un security officer dedicato le avevano lasciate in produzione per anni, perché nessuno di loro aveva mai realmente studiato in dettaglio la semantica delle sessioni PHP e le trappole specifiche della sua configurazione.
Questo articolo è il playbook delle vulnerabilità di sessione più ricorrenti che ho trovato in applicazioni PHP di produzione negli ultimi otto anni di attività offensive-security per banche, assicurazioni, fintech, portali pubblici e PMI di servizi digitali. Non è un'introduzione accademica - è il riassunto concreto di cosa cerco per primo quando faccio assessment, perché è lì che quasi sempre trovo il difetto grave. Il principio guida è uno: la sessione PHP sembra un meccanismo semplice gestito automaticamente dal linguaggio, ma la sua configurazione di default è insicura in almeno tre dimensioni e richiede tuning esplicito di ogni parametro per essere production-ready. Dare per scontato che "tanto PHP gestisce tutto" è il primo passo verso una compromissione reale.
Session fixation: l'attacco più elementare che trovo ancora in produzione nelle banche italiane
La session fixation è una famiglia di attacchi, formalmente classificata come CWE-384, in cui l'attaccante impone all'utente vittima un identificativo di sessione che l'attaccante già conosce, e poi - dopo che la vittima si è autenticata - riutilizza lo stesso identificativo per impersonare la vittima autenticata. Lo scenario canonico è questo. L'attaccante genera un ID di sessione valido connettendosi al sito target. Costruisce un link di phishing verso il sito target che contiene l'ID di sessione noto nel query string (es. https://banca.example.com/login?PHPSESSID=abcd1234efgh5678) e lo invia alla vittima. La vittima clicca il link, arriva sulla pagina di login del vero sito (è il sito legittimo!), si autentica con successo. L'applicazione, se configurata male, non rigenera l'ID di sessione al momento dell'autenticazione e continua a usare abcd1234efgh5678 come identificativo della sessione autenticata. L'attaccante, che conosce quell'ID dal giorno zero, lo riutilizza dal suo browser e si trova nell'area riservata con l'identità della vittima.
Il cuore tecnico di questa vulnerabilità è la combinazione di due configurazioni di default del PHP che sono entrambe insicure: primo, session.use_trans_sid = 1 (o lasciato all'impostazione legacy) permette il passaggio dell'ID di sessione via URL; secondo, session.use_only_cookies = 0 non impone che l'ID di sessione arrivi solo via cookie. Queste due configurazioni insieme permettono a chiunque di iniettare un ID di sessione arbitrario nell'URL della vittima. Il fix è cambiarle entrambe: session.use_trans_sid = 0 e session.use_only_cookies = 1 nel php.ini di produzione, in modo che PHP rifiuti categoricamente l'accettazione dell'ID di sessione via URL o form POST. La documentazione ufficiale di PHP sulle direttive di configurazione della sessione è molto dettagliata e va letta per intero in fase di hardening.
Il secondo pezzo del puzzle - quello che la banca italiana aveva sbagliato - è la rigenerazione dell'ID di sessione al momento dell'autenticazione. Anche se blocchi l'accettazione di ID arbitrari via URL, un attaccante può ancora creare una sessione "pulita" anonima sul tuo sito e convincere la vittima (con vari artifizi social-engineering) ad usarla. Il modo per sconfiggere strutturalmente l'attacco è rigenerare l'ID della sessione esattamente al passaggio da stato anonimo a stato autenticato - qualunque ID che l'attaccante avesse conosciuto prima diventa inutile subito dopo il login, perché il server ha assegnato un nuovo ID dopo l'autenticazione. In PHP si fa con session_regenerate_id(true) (il true è critico: elimina la vecchia sessione dal server). In Laravel 11 il metodo equivalente è $request->session()->regenerate() e va chiamato obbligatoriamente nel controller di login subito dopo la validazione delle credenziali. Sul portale della banca, il codice di login aveva session_regenerate_id() senza il true, il che manteneva la vecchia sessione valida - permetteva l'attacco in modo banale.
Se stai gestendo un'applicazione PHP con area autenticata e non hai mai fatto un assessment tecnico mirato sulla gestione delle sessioni, nel mio profilo professionale trovi il dettaglio delle attività di penetration testing e hardening session-security che ho condotto in contesti bancari, fintech e PMI con area riservata utente, sempre con approccio di remediation guidata e documentazione formale.
Session hijacking via cookie stealing: XSS, sniffing di rete, e configurazioni cookie insicure
La seconda famiglia di attacchi sulle sessioni è il session hijacking, in cui l'attaccante ruba l'ID di sessione di una vittima già autenticata, tipicamente esfiltrando il cookie di sessione dal suo browser. I vettori principali sono tre. Primo vettore: Cross-Site Scripting (XSS) riflesso o stored che esegue JavaScript nel browser della vittima e legge document.cookie. Secondo vettore: sniffing della rete su connessioni non cifrate - un attaccante sulla stessa rete WiFi della vittima intercetta il traffico HTTP in chiaro e ne estrae il cookie. Terzo vettore: malware lato client che legge i cookie del browser vittima direttamente dal filesystem.
La difesa contro il primo vettore si fa con l'attributo HttpOnly sul cookie di sessione, che istruisce il browser a non permettere accesso al cookie da JavaScript (protezione strutturale contro XSS che tenta di rubarlo). In PHP si attiva con session.cookie_httponly = 1 nel php.ini, o via session_set_cookie_params(['httponly' => true]) in codice. La difesa contro il secondo vettore si fa con l'attributo Secure sul cookie di sessione, che istruisce il browser a inviare il cookie solo su connessioni HTTPS - impedisce il downgrade a HTTP dove il cookie sarebbe in chiaro. Si attiva con session.cookie_secure = 1. Entrambe le configurazioni sono considerate baseline di produzione dal 2015 in poi, ma la mia esperienza pratica dice che il 40% delle applicazioni PHP PMI italiane le ha ancora disabilitate per errore di configurazione o per eredità storica di ambienti mixed HTTP/HTTPS.
La difesa contro il terzo vettore (malware client-side) è più sottile e richiede un meccanismo di binding della sessione al contesto del client. Il pattern che applico nei progetti più critici è un session fingerprint derivato da una combinazione di fattori relativamente stabili - user-agent del browser, prefisso dell'IP del client, eventuali header custom - hashato e memorizzato nella sessione server-side. A ogni richiesta, il server ricalcola il fingerprint dalla richiesta corrente e lo confronta con quello memorizzato; se cambia, la sessione viene invalidata. Questo meccanismo non blocca tutti gli attacchi di hijacking (un attaccante che spoofa user-agent e rete della vittima passa comunque), ma alza significativamente il costo di sfruttamento e cattura la maggioranza degli scenari reali. Il pattern di SameSite cookie attribute ben documentato nella reference MDN Web Docs aggiunge un ulteriore livello di protezione strutturale contro attacchi di cross-site request forgery intrecciati con hijacking di sessione.
Privilege escalation post-login: il pattern di rigenerazione ID a metà e i controller che non ne tengono conto
La terza classe di vulnerabilità legate alla sessione che trovo con frequenza preoccupante è la privilege escalation post-login - uno scenario in cui un utente legittimamente autenticato riesce a elevare i suoi privilegi manipolando lo stato della sessione in modo che l'applicazione non ha previsto. Il pattern specifico che avevo trovato alla banca italiana era questo: il sistema gestiva tre tipologie di login (utente retail, utente corporate, operatore filiale), ognuna con livelli di autorizzazione molto diversi, ma tutte condividevano lo stesso meccanismo di sessione. Il controller di login rigenerava l'ID al momento dell'autenticazione (abbiamo visto sopra che è corretto) ma non puliva i dati residui dalla sessione pre-login - incluso un campo user_role che alcune pagine pubbliche del portale impostavano temporaneamente a guest per ragioni di tracking analytics.
L'attacco concreto che ho dimostrato era questo. L'attaccante, da utente retail legittimamente autenticato (quindi con le sue credenziali reali, non rubate), navigava una specifica pagina pubblica che settava in sessione user_role = admin_demo come parte di un test A/B mai dismesso dal 2019. Poi si logava. Il login rigenerava l'ID ma non resettava user_role. Il controller post-login non setava esplicitamente il campo user_role a retail perché "tanto il ruolo viene dall'utente nel database". Ma un modulo sviluppato due anni dopo - che mostrava il menu admin se $_SESSION['user_role'] === 'admin_demo' - era stato rilasciato in produzione senza che nessuno verificasse l'interazione con il campo residuo. L'effetto combinato era che l'attaccante, post-login come utente retail, vedeva il menu admin e poteva accedere a funzionalità riservate.
Il fix strutturale è una disciplina operativa: ogni controller di login deve resettare esplicitamente la sessione a uno stato pulito subito dopo il session_regenerate_id(true), ricostruendo solo i campi che sono esplicitamente attesi post-autenticazione. In Laravel il pattern è $request->session()->flush() seguito da $request->session()->regenerate() e dal populamento controllato dei soli campi necessari. Questo esempio sottolinea un principio generale dell'hardening applicativo che richiamo nel mio articolo su analisi forense di un attacco Laravel con kill chain su log e filesystem: le vulnerabilità più gravi emergono dall'interazione fra componenti scritti in momenti diversi da persone diverse, e solo un approccio sistematico di pulizia dello stato di sessione al login le previene strutturalmente.
Session storage su Redis condiviso e sticky session in architetture multi-server
Il quarto tema operativo delle sessioni PHP riguarda l'infrastruttura di storage della sessione in deployment multi-server. Il driver di default files memorizza le sessioni sul filesystem locale del server che ha servito la richiesta. In un deploy single-server funziona perfettamente. In un deploy multi-server (autoscaling orizzontale, cluster load-balancer) produce un problema: l'utente che riconnette dopo essersi loggato può finire su un server diverso da quello che ha la sua sessione salvata, e si ritrova sloggato senza motivo apparente. Le due soluzioni possibili sono la sticky session al load balancer (le richieste dello stesso utente vanno sempre allo stesso backend, identificato tramite cookie tecnico del LB) o un session storage condiviso fra tutti i backend (tutti i server leggono e scrivono le sessioni sullo stesso Redis condiviso).
La mia preferenza strutturale è quasi sempre session storage condiviso su Redis, non sticky session. Le ragioni sono tre. Primo, sticky session crea hot spot operativi - se un singolo backend si satura, gli utenti "incollati" a quel backend soffrono in modo sproporzionato. Secondo, sticky session impedisce il decommissioning sicuro di backend durante deploy rolling - per riavviare un backend devi prima drenare le sue sticky session, aspettare che scadano, e solo dopo puoi riavviare. Terzo, sticky session non sopravvive a cambi di IP del client (mobile che passa da WiFi a 4G), causando logout inaspettati. Redis condiviso non ha nessuna di queste patologie: ogni richiesta può essere servita da qualunque backend disponibile, il LB può fare routing puro senza persistenza di session state. Il setup Laravel per session storage su Redis richiede tre righe nel .env - SESSION_DRIVER=redis, SESSION_CONNECTION=sessions, più la configurazione del REDIS_URL - e funziona out of the box. Questo pattern è il prerequisito dell'architettura di autoscaling che ho descritto in dettaglio nel mio articolo sui Droplet Digital Ocean per Laravel con sizing corretto e autoscaling senza Kubernetes, dove la session state esternalizzata è la chiave che rende possibile la distruzione e ricreazione dinamica dei server web senza impatto per gli utenti.
Il risultato finale del penetration test sulla banca italiana, dopo nove giornate totali di assessment e una successiva sessione di remediation affiancata al team interno durata altre 12 giornate, è stato il seguente. Session fixation rimossa strutturalmente (session.use_trans_sid=0, session.use_only_cookies=1, session_regenerate_id(true) dopo ogni login). Session hijacking reso significativamente più difficile grazie a HttpOnly, Secure, SameSite=Lax, più session fingerprint binding che ha aumentato il costo di hijacking in scenari realistici. Privilege escalation via session state residua eliminata tramite flush esplicito della sessione a ogni login e disciplina di code review che richiede il pattern in ogni nuovo controller di autenticazione. Session storage migrato a Redis condiviso, preparando strutturalmente il terreno per scalabilità orizzontale pianificata nel 2026. Audit log esteso per tracciare ogni operazione di login, logout, rigenerazione di sessione, con detection automatica di anomalie. Il report finale consegnato al CRO ha incluso tutti i PoC dimostrativi, la remediation implementata, e un set di regole di code review da applicare in ogni futuro cambiamento al layer di autenticazione.
Se gestisci un'applicazione PHP con area autenticata che tratta dati personali, finanziari, sanitari, o comunque informazioni sensibili, e non hai mai commissionato un assessment tecnico mirato sulla gestione delle sessioni, ti trovi molto probabilmente con almeno una delle vulnerabilità descritte in questo articolo, senza saperlo. La probabilità cumulativa che un applicazione PHP senza audit dedicato abbia almeno una session vulnerability sfruttabile è superiore al 75% secondo le mie statistiche su circa 60 assessment degli ultimi otto anni. Se vuoi confrontarti sul tuo caso specifico con un assessment mirato sulla sicurezza delle sessioni del tuo portale PHP, contattami per una consulenza iniziale: in due-tre giornate di penetration test focalizzato produco un report dettagliato con i vettori di attacco reali contro la tua applicazione, i proof-of-concept dimostrativi per ciascuno, e una roadmap di remediation prioritizzata sulla gravità effettiva delle esposizioni rilevate.