Come funziona la use-after-free di PHP2Shell nelle CVE-2026-109618 e CVE-2026-109619

Come funziona la use-after-free di PHP2Shell nelle CVE-2026-109618 e CVE-2026-109619

Un processo di PHP che serve il web non nasce e muore con la richiesta che sta servendo. Nasce quando parte il servizio, resta in vita per centinaia o migliaia di richieste, e ogni volta deve fingere di essere appena nato. Quella finzione è il tratto distintivo del linguaggio: lo sviluppatore scrive codice come se il mondo iniziasse all'inizio dello script e finisse alla fine, e il motore si occupa di rendere vera quell'illusione pulendo tutto in mezzo. È il motivo per cui un'applicazione PHP non perde memoria dopo tre giorni di esercizio come farebbe un processo scritto con la stessa disinvoltura in un altro linguaggio.

CVE-2026-109618 e CVE-2026-109619, le due vulnerabilità corrette il 21 ottobre 2026 e note insieme come PHP2Shell, entrambe valutate 9.2 su CVSS 4.0, stanno nel punto preciso in cui quella finzione si rompe. Sono il crollo di un'assunzione che attraversava l'intero motore, e la correzione che le chiude entrambe tocca 389 file del codice sorgente: quel numero è la misura di quanto l'assunzione fosse diffusa.

Questo è il pezzo concettuale della serie. Non c'è niente qui che serva a riprodurre il problema, e non serve: quello che serve a chi amministra un server è capire perché le contromisure di configurazione proposte negli avvisi siano tutte dichiarate incomplete, e quella risposta sta nella forma del difetto, non nei suoi dettagli. La parte operativa, cioè come verificare su quale codice gira davvero il tuo interprete, sta nell'articolo precedente della serie.

Che cosa significa usare memoria dopo averla liberata

La classe di difetto si chiama use after free e non è specifica di PHP: appartiene a qualunque programma scritto in un linguaggio dove la memoria si alloca e si libera esplicitamente, quindi al motore di PHP, che è scritto in C, e non al codice PHP che ci gira sopra.

Il meccanismo, ridotto all'osso, è questo. Un programma chiede al sistema un'area di memoria per metterci qualcosa, tiene da parte l'indirizzo di quell'area, e quando ha finito la restituisce. Da quel momento l'area non è più sua: il gestore della memoria la considera libera e la riassegnerà alla prima richiesta utile. L'indirizzo, però, continua a esistere nella variabile che lo conteneva. Se qualcosa in seguito usa quell'indirizzo, legge o scrive in un'area che nel frattempo appartiene a qualcos'altro.

Il difetto non sta nel liberare la memoria e non sta nel tenerne l'indirizzo. Sta nella distanza fra i due momenti, cioè in un pezzo di codice che assume di essere ancora proprietario di qualcosa che non possiede più.

La gravità dipende interamente da cosa finisce in quell'area nel frattempo, ed è la ragione per cui difetti di questa classe hanno esiti così diversi fra loro. Nel caso fortunato il programma legge valori privi di senso e termina. Nel caso sfortunato quell'area contiene, per come si è disposta la memoria in quel momento, qualcosa che il programma interpreta come un'istruzione su cosa fare dopo. Fra i due estremi c'è una zona intermedia in cui il contenuto viene semplicemente restituito a chi ha fatto la richiesta, e allora il difetto diventa una perdita di informazioni: dentro quella memoria c'era quello che stava facendo qualcun altro un istante prima.

Gli avvisi del progetto PHP, GHSA-w8hw-w9fx-j79w e GHSA-6wmf-4rv5-p3rp, classificano entrambe le vulnerabilità sotto questa categoria e ne elencano le condizioni con una precisione che è raro trovare in un bollettino. Se ti serve qualcuno che traduca documenti del genere in decisioni per la tua infrastruttura invece di girarti il link, nel mio profilo professionale trovi il lavoro che faccio su sicurezza applicativa e manutenzione di stack PHP in produzione.

Perché un interprete che ricicla i processi rende tutto più fragile?

Perché la pulizia fra una richiesta e l'altra non è un'operazione accessoria ma la condizione che tiene in piedi l'illusione di partire da zero, e quella pulizia ha un percorso privilegiato che per velocità salta i controlli individuali.

Qui conviene spiegare come funziona la gestione della memoria nel motore, perché è una scelta di progetto brillante ed è anche la ragione della fragilità. PHP non libera la memoria di una richiesta pezzo per pezzo alla fine dello script. Alloca la memoria della richiesta dentro aree dedicate e, al termine, le restituisce in blocco. È un'operazione molto rapida e intrinsecamente ordinata: non importa quante strutture ci fossero dentro, finiscono tutte insieme.

Il prezzo di questa scelta è che la fine di una richiesta è un confine netto e totale. Tutto quello che stava nella memoria della richiesta smette di esistere nello stesso istante. Se qualcosa, da qualche parte, continua a puntare lì dentro, da quell'istante punta nel vuoto. E il motore ha diverse cose che vivono più a lungo di una singola richiesta, per necessità: lo stato di configurazione, il registro delle estensioni caricate, le informazioni sull'ultimo errore, le strutture delle sessioni. Sono dette globali perché appartengono al processo, non alla richiesta, e la regola implicita è che non devono mai trattenere un riferimento a memoria della richiesta oltre la fine della richiesta stessa.

Finché tutto procede in modo ordinato, la regola viene rispettata, perché ogni operazione che pubblica qualcosa in una struttura globale ha, da qualche parte, l'operazione corrispondente che la ripulisce prima della fine.

Che cosa succede quando una richiesta finisce male

La risposta è il cuore della faccenda. Quando una richiesta incontra un errore fatale, PHP non torna indietro ordinatamente dalla catena delle chiamate come farebbe un linguaggio con le eccezioni. Usa un meccanismo del C che permette di saltare da un punto qualunque del programma direttamente a un punto stabilito molto più in alto, scavalcando tutto quello che sta in mezzo.

Quel salto è efficiente e, per vent'anni, è stato considerato innocuo, perché l'assunzione implicita era che il punto di arrivo sapesse mettere ordine. La correzione pubblicata dimostra che l'assunzione non reggeva in due modi diversi, e le due vulnerabilità sono le due facce di questo.

Le due vulnerabilità a confronto
CaratteristicaCVE-2026-109618CVE-2026-109619
Si manifesta in una sola richiestaSìNo, serve una richiesta successiva
Coinvolge stato che sopravvive al processoNoSì
Esecuzione dimostrata su Apache con modulo integratoSìSì
Esecuzione dimostrata su interprete in processo separatoNo, analisi del sorgenteSì
Esiste una contromisura di configurazione completaNoNo
Fonte: Avvisi GHSA-w8hw-w9fx-j79w e GHSA-6wmf-4rv5-p3rp, ottobre 2026Verificato:

La prima riguarda il momento sbagliato. Durante la preparazione di una richiesta ci sono istanti in cui una struttura è già raggiungibile ma non ancora completa: è stata annunciata al resto del motore, e il suo riempimento è in corso. Se il salto avviene proprio in uno di quegli istanti, la procedura di chiusura trova una struttura a metà e la tratta come se fosse finita. Il dettaglio che rende la cosa scomoda è che l'allocazione che fallisce non ha alcun rapporto con la struttura incompleta: basta che la memoria finisca mentre quella finestra è aperta, e la memoria può finire per qualunque motivo.

La seconda riguarda il residuo. Un'operazione che aveva pubblicato qualcosa in una struttura globale viene interrotta prima di arrivare alla propria pulizia. La richiesta finisce comunque, la sua memoria viene restituita in blocco, e la struttura globale resta a puntare a un'area che adesso è libera. Il processo, intanto, è vivo e accetta la richiesta successiva. Se qualcosa, nelle primissime fasi di quella nuova richiesta, consulta quella struttura globale prima che la normale inizializzazione l'abbia azzerata, legge un residuo.

Nota

La seconda condizione non richiede che l'applicazione faccia nulla di particolare, e non richiede nemmeno che faccia qualcosa di sbagliato. Richiede che due richieste finiscano sullo stesso processo e che la prima si sia interrotta in modo brusco: su un server che ricicla i worker, entrambe le cose sono normali.

Questo spiega anche perché l'avviso insista sul fatto che una parte di ciò che accade avviene prima che il codice dell'applicazione entri in gioco. L'elaborazione dei dati in ingresso di una richiesta è lavoro che il motore fa da solo, prima di eseguire una sola riga del tuo script. Un'applicazione che non ha una riga di codice scritta male è nella stessa posizione di una scritta malissimo.

Perché la correzione tocca centinaia di file

La risposta sta nel messaggio con cui è stata pubblicata, che in due righe dichiara entrambe le metà del lavoro: sostituire il salto brusco in caso di memoria esaurita, e azzerare in modo sistematico le strutture globali che possono venire consultate durante l'avvio di una richiesta. Non si poteva correggere il punto in cui il difetto si manifesta, perché i punti erano troppi. La correzione ha cambiato la regola del gioco: ha eliminato il salto brusco per la condizione di memoria esaurita.

Il nuovo comportamento è a due soglie, e vale la pena capirlo perché cambia quello che vedi nei log.

Come il motore gestisce una richiesta di memoria dopo la correzione
  1. Il codice chiede memoria al gestore
  2. La richiesta supera il limite configurato?

    Sì:

    1. Tenta di recuperare memoria non più usata

    No:

    1. Alloca e prosegue

    Fine

  3. Dopo il recupero si resta oltre il limite?

    Sì:

    1. Mette in coda un errore da sollevare al prossimo punto sicuro

    No:

    1. Alloca e prosegue

    Fine

  4. Si supera anche la seconda soglia, più alta?

    Sì:

    1. Il processo termina subito, senza proseguire in stato incerto

    No:

    1. La richiesta si chiude in modo ordinato all'errore in coda

La differenza sostanziale sta nella parola punto sicuro. Invece di interrompere l'esecuzione nell'istante esatto in cui l'allocazione fallisce, il motore prende nota e solleva l'errore poco dopo, in un punto in cui sa che nessuna struttura è a metà. L'effetto osservabile per chi gestisce un server è che il limite di memoria continua a funzionare come prima, cioè la richiesta che sfora viene terminata, ma la terminazione avviene da un punto noto.

La seconda soglia esiste perché la prima, essendo diventata morbida, lascia al motore un margine di consumo oltre il limite nominale. Quel margine è limitato: la soglia alta è predefinita al doppio del limite configurato, e sopra il gigabyte diventa il limite più un gigabyte. Superarla termina il processo in modo immediato, scrivendo una riga di diagnostica che prima non esisteva. È l'unico caso in cui, dopo l'aggiornamento, un worker muore senza chiudere la richiesta in modo ordinato, ed è una scelta deliberata: meglio un processo in meno che un processo che prosegue in uno stato di cui non ci si può fidare.

La seconda metà della correzione riguarda il residuo, e consiste nell'azzerare in modo sistematico tutto lo stato di processo che poteva sopravvivere a una richiesta interrotta. Non c'era un singolo punto da correggere, c'erano tutti i punti in cui qualcuno aveva assunto che la propria pulizia sarebbe sempre stata eseguita.

Perché nessuna contromisura di configurazione è sufficiente

A questo punto la risposta viene da sé, ed è il motivo per cui vale la pena aver fatto tutto questo giro.

Le contromisure proposte negli avvisi agiscono sui modi noti per arrivare alla condizione. Tenere basso il limite sulla dimensione del corpo delle richieste rende più difficile far consumare molta memoria dall'esterno. Imporre tetti al numero di campi e al livello di annidamento riduce il lavoro che il motore fa prima di cedere il controllo all'applicazione. Disattivare il meccanismo di avanzamento degli upload nelle sessioni toglie di mezzo una delle strutture che potevano restare pubblicate. Sul ramo 8.5, disattivare le tracce di chiamata sugli errori fatali ne toglie un'altra.

Sono tutte misure sensate e nessuna chiude il problema, perché nessuna agisce sulla classe: agiscono sui percorsi noti per arrivarci. La condizione di memoria esaurita è uno stato in cui un processo può finire per molte strade, incluse quelle che dipendono dall'applicazione o da un'estensione, e uno stato non si filtra come si filtra un ingresso. E la condizione del residuo non ha nemmeno bisogno di memoria esaurita: le basta un'interruzione brusca, che può arrivare da un errore di compilazione, da un'inclusione fallita o dal superamento del tempo massimo di esecuzione.

C'è poi una misura che gli avvisi raccomandano e che va presa con una cautela in più, perché fa l'opposto di quello che l'istinto suggerisce: togliere del tutto il limite di memoria. L'idea è che un limite per richiesta prevedibile è proprio ciò che rende la condizione raggiungibile su comando, mentre senza limite la memoria finisce solo quando finisce davvero. Ha senso, e porta con sé che una singola richiesta può consumare tutta la memoria della macchina, quindi va accompagnata da un contenimento esterno. Gli avvisi sono espliciti nel dire che i vecchi limiti sulle risorse del processo non vanno usati a questo scopo, perché producono proprio il fallimento di allocazione che si voleva evitare.

Quando introduci limiti di questo tipo a livello di sistema o di web server, la cosa che conviene fare prima è leggere la configurazione che hai già invece di aggiungerne una nuova sopra: il linter di configurazione per Apache e Nginx è un modo rapido di vedere cosa è impostato davvero e dove.

Che cosa cambia per chi scrive codice PHP

Quasi nulla, ed è bene dirlo chiaramente invece di trasformare ogni avviso di sicurezza del motore in una colpa degli sviluppatori. Nessuna pratica di programmazione in PHP causa queste vulnerabilità e nessuna le evita.

Tre cose, però, hanno un effetto indiretto che vale la pena conoscere, e sono tutte buone pratiche anche senza questa vicenda.

La prima è ridurre gli errori fatali raggiungibili dall'esterno. Un percorso che un visitatore non autenticato può far fallire in modo brusco è il presupposto della seconda condizione. Vale per i percorsi evidenti e soprattutto per quelli che si guardano di meno: elaborazione di file caricati, importazioni, pagine di errore, distruttori, gestori di output, codice delle estensioni. Il tempo massimo di esecuzione raggiunto va considerato come qualunque altro errore fatale.

La seconda è il riciclo dei processi. Ogni ambiente con interprete in processo separato ha un parametro che decide dopo quante richieste un worker viene sostituito. Abbassarlo riduce la probabilità che due richieste consecutive scelte da un attaccante finiscano sullo stesso processo, al prezzo di un po' di prestazioni, ed è una delle leve descritte nella guida al tuning dei pool PHP-FPM sotto carico. Resta una riduzione di probabilità, non una chiusura.

La terza è l'igiene dei messaggi di diagnostica emessi prima dell'esecuzione dello script. Impostazioni deprecate o incoerenti applicate per singola directory producono segnalazioni nelle primissime fasi di una richiesta, cioè esattamente nella finestra in cui una struttura globale non è ancora stata reinizializzata. Rimuovere le direttive obsolete dalle configurazioni per directory è manutenzione ordinaria che quasi nessuno fa, e in questo caso è anche riduzione di superficie. Va detto che nasconderle non serve: le impostazioni che controllano se un messaggio viene mostrato o registrato agiscono dopo che lo stato è stato consultato.

Importante

La diagnostica non deve essere visibile, né registrata, né di livello grave perché la condizione si verifichi. Disattivare la visualizzazione degli errori è una buona pratica per altri motivi e non ha effetto su questo.

Il motivo per cui ho scritto questo pezzo è che la differenza fra una vulnerabilità applicativa e una del motore si vede proprio qui. Una vulnerabilità applicativa la correggi dove l'hai scritta, e nel frattempo puoi contenerla con una regola di filtro, perché conosci la forma dell'ingresso che la attiva. Una vulnerabilità che nasce da un'assunzione del motore non ha una forma: ha tanti modi di presentarsi quante sono le strade per arrivare a quello stato, e chiunque ti venda una contromisura come equivalente dell'aggiornamento ti sta vendendo la chiusura di una strada su un numero di strade che nessuno ha contato.

La cronaca della scoperta e i controlli da fare sulle macchine stanno nel pezzo sulla scoperta di PHP2Shell, che apre la serie. I tre pezzi che seguono guardano la stessa vicenda dal lato opposto, cioè dai dati: in quanto tempo le sonde automatiche arrivano a cercarlo, come si distingue quel traffico dal rumore di fondo e cosa è arrivato nelle prime quarantotto ore.

Se hai in produzione applicazioni PHP di cui non conosci né le versioni né i percorsi che possono fallire, quello è un problema che viene prima di qualunque avviso di sicurezza. Scrivimi: l'interprete è solo uno degli strati da mettere in sicurezza, e insieme a quello guardo web server, database, sistema operativo e codice dell'applicazione, perché una falla come questa si mitiga sul perimetro intero e non su un pezzo scelto per comodità.

Ultima modifica: