Il 21 ottobre 2026 il PHP Security Team ha pubblicato un avviso di sicurezza fuori calendario e quattro versioni d’emergenza. Se gestisci anche un solo sito che gira su PHP, WordPress compreso, questo post ti riguarda: cosa è successo, cosa devi fare, e come si verifica di essere patchati davvero, visto che il numero di versione non te lo dice.

In sintesi

Cosa è successo. Due vulnerabilità nel motore di PHP, battezzate insieme PHP2Shell, permettono a un attaccante di far eseguire codice sul tuo server senza bisogno di avere un account, una password o un link su cui farti cliccare. Basta una richiesta web costruita ad arte verso un sito che gira su PHP. Gravità CVSS 9.2 su 10.

Chi è interessato. Qualunque installazione PHP che serva richieste web. Non è un problema di WordPress, di Drupal o di un plugin: il difetto sta nel linguaggio, sotto l’applicazione. Un sito con codice scritto a regola d’arte è esposto esattamente come uno scritto male.

Cosa devi fare, in ordine.

  1. Portare PHP a 8.5.12, 8.4.27, 8.3.36 o 8.2.35, o alla versione corretta che fornisce il tuo hoster o la tua distribuzione.
  2. Riavviare Apache, PHP-FPM e ogni processo che esegue PHP, container inclusi. Senza riavvio il codice vulnerabile resta in memoria e continui a essere attaccabile.
  3. Verificare che la patch ci sia davvero, e qui il numero di versione non basta: è il tema di questo post.
  4. Se sei su PHP 8.1 o precedenti, sei su un ramo che il progetto PHP non aggiorna più: ti serve il backport del tuo fornitore, oppure salire di versione.

Quanto è urgente. Molto. I dettagli tecnici sono usciti il 20 ottobre, prima che la patch esistesse, e i PoC pubblici circolano. Trattalo come un intervento da fare oggi, non da mettere nella pianificazione della settimana.

Cosa dice l’avviso ufficiale

Comincio dalla fonte primaria invece che da un riassunto. Dall’avviso del PHP Security Team del 21 ottobre 2026, intitolato Security advisory: Critical remote code execution vulnerabilities, update now:

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two critical vulnerabilities that can allow unauthenticated remote code execution. All PHP users are strongly encouraged to update immediately.

E sul perimetro di chi è colpito, che è la parte che conviene leggere due volte:

Who is at risk: all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. We consider the risk highest for mod_php, with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs.

Tradotto nella parte che conta: un attaccante non autenticato può far scattare l’attacco prima che venga eseguita una sola riga del codice applicativo. Nessun controllo scritto nel tuo PHP può difenderti, perché quando l’attacco parte il tuo PHP non è ancora stato eseguito.

Le due vulnerabilità, per chi vuole il dettaglio

CVE-2026-109618, RCE via OOM corruption (GHSA-w8hw-w9fx-j79w). Quando PHP esaurisce la memoria interrompe la richiesta a metà. In alcuni contesti quell’interruzione lascia i dati interni della richiesta inizializzati solo in parte, e la pulizia di fine richiesta li tratta come validi. L’avviso lo descrive così:

out-of-memory handling can interrupt PHP while internal request data is only partially initialized. Request cleanup may then treat the invalid data as valid.

È classificata CWE-824, accesso a puntatore non inizializzato.

CVE-2026-109619, RCE via stale global state (GHSA-6wmf-4rv5-p3rp). Dopo un errore fatale, lo stato interno che apparteneva alla richiesta può sopravvivere alla liberazione della memoria che lo conteneva. Una richiesta successiva, servita dallo stesso worker, lo usa prima che l’inizializzazione normale lo azzeri:

request-owned internal state can survive after its backing memory has been freed. A later request handled by the same worker may use that stale state before normal initialization resets it.

Qui sta il motivo per cui i worker persistenti sono il caso peggiore: su Apache in prefork il puntatore sopravvive tra richieste diverse, quindi tra utenti diversi. La complessità d’attacco però è più alta, perché serve far riallocare dati controllati esattamente nello spazio liberato, e se il timing non coincide il worker va in crash invece di eseguire.

Lo stato dello sfruttamento, senza esagerare

Su questo punto le fonti non concordano, e visto che molti articoli stanno scrivendo “già sfruttata attivamente” come se fosse un fatto accertato, vale la pena essere precisi.

watchTowr, che ha riprodotto entrambe le vulnerabilità, dichiara nella sua pagina aggiornata al 21 ottobre: «watchTowr is observing active exploitation attempts of both CVE-2026-109618 and CVE-2026-109619», e nei suoi Key Facts riporta «Exploited in the wild: Yes, reported by watchTowr».

Rapid7, nell’aggiornamento del 22 ottobre, è più cauta: «Rapid7 has not observed exploitation in the wild, though there are multiple reports of exploitation attempts». Sui proof of concept pubblici aggiunge che ciascuno funziona solo per una precisa combinazione di sistema operativo, versione di PHP e web server, e che «none appears to be broadly reliable».

La sintesi onesta è questa: tentativi segnalati sì, exploit universale e affidabile non ancora. Non è un motivo per rilassarsi, è un motivo per muoversi adesso, perché la distanza tra “PoC fragile” e “exploit affidabile” si misura in giorni. A rafforzare l’urgenza c’è un dato indipendente: Amazon ha dichiarato che la sua rete di honeypot MadPot aveva registrato un tentativo di sfruttamento già il 17 ottobre, quattro giorni prima della divulgazione pubblica.

Se non puoi aggiornare subito

L’avviso ufficiale elenca mitigazioni temporanee, ma le incornicia con una frase che va letta per intera prima di affidarcisi:

while only an update is a complete solution, the following measures reduce exposure to specific attack paths but do not make an installation safe

Le principali, dall’avviso:

  • Per la 109618: impostare memory_limit=-1 in modo che le applicazioni non possano sovrascriverlo, per esempio con php_admin_value nel pool FPM o nella configurazione Apache. È controintuitivo, perché l’istinto direbbe di abbassare il limite, non di toglierlo: togliere il limite elimina il punto di fallimento controllabile dall’attaccante. Resta però che un fallimento di allocazione sottostante può comunque arrivare al percorso vulnerabile. In più, abbassare post_max_size (8M senza upload, 32M con upload) e tenere upload_max_filesize sotto quel valore.
  • Per la 109619: session.upload_progress.enabled=0 applicato da php.ini, pool FPM o configurazione del server, e riavvio di ogni worker. L’avviso è esplicito sul fatto che non basta ini_set() o un .user.ini di FPM, e che questo chiude i percorsi di sessione confermati «but not other variants». Su PHP 8.5, in aggiunta, fatal_error_backtraces=0.

Nessuna di queste è una correzione. Sono riduzioni di superficie per guadagnare le ore che servono ad aggiornare.

Versioni corrette: 8.5.12, 8.4.27, 8.3.36, 8.2.35. Per 7.4, 8.0 e 8.1 il team di sicurezza ha pubblicato backport minimi di sorgente, pensati per chi compila in proprio o non ha un fornitore LTS alle spalle.

Il punto che gli avvisi non dicono: la versione non è una prova

Qui parte la parte utile, cioè quella che mi è costata tempo.

Caso 1. Hosting condiviso con CloudLinux: patchato, ma con il numero di versione di prima

Un sito cliente su hosting condiviso Keliweb, cPanel con CloudLinux e alt-php. Qui la parte buona della storia: la correzione è arrivata lo stesso giorno, con un backport CloudLinux, e l’apertura del ticket interno risultava alle 13:04. Su una RCE non autenticata già sfruttata, un hoster che si muove in giornata è esattamente quello che vuoi.

Il link a Keliweb è di affiliazione: se attivi un servizio da lì io prendo una commissione, tu paghi lo stesso. Lo uso perché ci tengo davvero dei siti, e in questo caso hanno patchato in giornata.

Il risultato però è che il numero di versione non è cambiato. Prima 8.5.11, dopo 8.5.11.

Se avessi controllato con php -v avrei letto una versione che tutte le tabelle degli avvisi marcano come vulnerabile, su un server già corretto. E se avessi letto quel numero il giorno prima avrei visto esattamente la stessa stringa su un server davvero vulnerabile. Lo stesso output per due stati opposti.

È il comportamento normale dei backport: RHEL, Debian, Ubuntu e CloudLinux correggono dentro la stessa versione upstream, perché il loro contratto è la stabilità dell’ABI, non l’allineamento al numero di versione. Non è un’anomalia dell’hoster, è il modello. Lo mette in conto anche l’avviso ufficiale, che per le distribuzioni rimanda al fornitore invece che al numero di versione:

Linux distributions and LTS vendors received the patches in advance, so check your vendor for updated packages.

“Check your vendor” è esattamente il punto: il PHP Security Team sa che su quei sistemi la versione che leggi non corrisponde a quella del progetto.

Quello che funziona è guardare quando è stato costruito il binario:

# La data di build del binario, non la versione
php -i | grep -i 'build date'

# Su CloudLinux i SAPI sono binari separati: la CLI non dice
# niente sul PHP che serve il web. Vanno guardati tutti.
ls -l --time-style=full-iso /opt/alt/php85/usr/bin/php \
                            /opt/alt/php85/usr/bin/php-cgi \
                            /opt/alt/php85/usr/sbin/lsphp

Sul server in questione i binari di alt-php 8.5, 8.4, 8.3 e 8.2 (CLI, php-cgi e lsphp) risultavano compilati il 21 ottobre tra le 20:04 e le 20:39. La prova che la sostituzione fosse effettivamente avvenuta sulla macchina è arrivata dal confronto con una lettura precedente: alle 00:14 del 22 ottobre la stessa CLI dichiarava ancora built Oct 6. Due letture a distanza di ore, due date di build diverse, nessun cambio di versione.

Il changelog rpm, che sarebbe la fonte più diretta, da utente cPanel non era leggibile. Il confronto delle date di build è stato l’unico controllo disponibile dall’interno.

La lezione operativa: su un hosting gestito la domanda da fare al supporto non è “che versione di PHP avete”, è “quando avete ricostruito i binari e avete riavviato i worker”.

Aggiornamento del 23 ottobre 2026. Quello che ho descritto qui sopra è lo stato del 22 ottobre. In serata Keliweb è passata ad alt-php 8.5.12, cioè alla versione upstream vera, con la CLI compilata alle 18:50 del 22 ottobre. Se controlli adesso un sito su quel fornitore leggi 8.5.12, non l’8.5.11 di cui parlo sopra.

Il passaggio non smentisce la tesi del post, la illustra meglio di come avrei saputo fare io. In poco più di trentasei ore quello stesso server ha attraversato tre stati diversi: binario del 6 ottobre con versione 8.5.11, cioè vulnerabile; binario del 21 ottobre ancora con versione 8.5.11, cioè patchato col backport; binario del 22 ottobre sera con versione 8.5.12, patchato e finalmente allineato. Il numero di versione distingueva solo il terzo stato. La data di build li distingueva tutti e tre.

E vale anche adesso che il numero è quello giusto: 8.5.12 è una buona notizia sul pacchetto installato, non sui processi in esecuzione. Finché i worker non sono stati riavviati possono ancora eseguire il codice di prima.

Caso 2. Docker: l’immagine ufficiale è in ritardo sul runtime

Secondo ambiente, un VPS con una webmail Roundcube in container, installata dal marketplace di 1Panel. Qui il problema è diverso: l’immagine ufficiale dell’applicazione è costruita su un tag PHP che, al momento in cui mi serviva, non era ancora quello patchato. Aspettare il rebuild di upstream voleva dire restare esposti per un tempo deciso da altri, con l’exploit già in circolazione.

La scelta è stata ricostruire l’immagine sul posto, partendo dal Dockerfile ufficiale e cambiando una cosa sola: la base, portata a php:8.4.27-apache.

Il “una cosa sola” è deliberato. Il Dockerfile al commit che ho usato puntava a una versione di Roundcube più recente di quella in produzione, e l’ho forzata a restare quella installata. In una patch di sicurezza urgente si cambia il runtime o si cambia l’applicazione, mai entrambi: se qualcosa si rompe devi sapere di chi è la colpa, e devi saperlo in minuti, non in mezz’ora di bisezione.

Estensioni PHP identiche all’immagine ufficiale, backup della cartella app e dump del database prima di toccare il compose, e il compose precedente salvato accanto al backup. Il rollback è una copia di file e un up -d.

Caso 3. I pannelli rimettono l’immagine ufficiale al prossimo upgrade

Questo è il dettaglio che trasforma una patch in un debito. I marketplace dei pannelli (1Panel nel mio caso, ma il meccanismo è lo stesso su piattaforme simili) rigenerano i file di configurazione delle app a ogni aggiornamento, e rimettono l’immagine ufficiale al posto della tua.

Quindi la patch che ho appena applicato ha una data di scadenza ignota: il giorno in cui qualcuno accetta un upgrade dell’app dal pannello, il PHP torna quello dell’immagine upstream. Se nel frattempo upstream ha ricostruito va bene, altrimenti si torna indietro in silenzio, senza nessun avviso, perché dal punto di vista del pannello è andato tutto liscio.

L’unica contromisura che ho trovato onesta è scritta, non tecnica: una riga nella checklist post-upgrade dello stack, che dice di verificare la versione di PHP dell’immagine ufficiale prima di accettare l’aggiornamento, e di rimettere l’immagine custom se è ancora indietro. Più la nota di tornare all’ufficiale, e cancellare la custom, quando upstream si allinea. Una patch custom che nessuno ha il compito di rimuovere diventa l’ambiente legacy di qualcun altro tra due anni.

La trappola WAF: una scorciatoia proprio dove passa l’exploit

Questa è la cosa che non ho visto scritta da nessuna parte e che vale il post più di tutto il resto.

Cloudflare ha distribuito regole WAF per i vettori noti, disponibili anche sui piani gratuiti, e altri vendor hanno fatto lo stesso con le loro virtual patch. Ottimo, e va detto subito che sono un controllo compensativo, non una correzione: mitigano i pattern conosciuti, non il difetto.

Il problema è che un WAF ha anche le regole che hai scritto tu. Su una delle zone che gestisco c’era una regola di skip del WAF su un insieme di azioni della webmail: upload, send, compose. Una regola del tutto ragionevole quando la scrivi, perché i falsi positivi su un allegato grosso sono una rogna e la webmail è dietro autenticazione.

Ora metti insieme i due pezzi. Tre delle cinque tecniche di sfruttamento passano dall’upload progress delle sessioni, con richieste multipart costruite. La mia regola disattivava il WAF esattamente sulle richieste di upload. Avevo scritto a mano una scorciatoia attraverso le protezioni nuove, mesi prima che le protezioni esistessero.

Vai a guardare le tue regole di skip, bypass e page rule prima di fidarti delle regole WAF appena distribuite. Il punto non è quella specifica regola: è che ogni eccezione scritta in passato per far passare un caso legittimo va rivalutata quando cambia il modello di minaccia. Nel mio caso la conclusione è che il rischio è coperto dal runtime patchato, non dal WAF, e il WAF è tornato a essere quello che è: uno strato in più, non la difesa.

Vale la pena ripetere qui quello che l’avviso ufficiale dice delle mitigazioni di configurazione viste sopra: riducono l’esposizione a percorsi d’attacco specifici, ma «do not make an installation safe». Se l’unica cosa che puoi toccare è la configurazione, considerati ancora vulnerabile e organizzati per aggiornare.

Il riavvio non è un dettaglio di contorno

Aggiornare i pacchetti non sostituisce il codice già mappato nei processi in esecuzione. Finché non riavvii, i worker continuano a eseguire il PHP vulnerabile che hanno in memoria, e tutti i controlli sul filesystem dicono che sei a posto.

Vanno riavviati Apache, PHP-FPM e qualunque altro processo che esegua PHP, i container compresi. Nel caso Docker il up -d che ricrea il container fa già il lavoro. Sul resto va fatto apposta, e verificato dopo, perché è il passaggio che si salta quando si va di fretta.

Come verificare davvero: i comandi

# 1. La versione, che serve solo come primo indizio
php -v

# 2. La data di build, che è il dato che conta
php -i | grep -i 'build date'

# 3. Il SAPI del web NON è la CLI: va verificato a parte.
#    Su FPM:
php-fpm -i 2>/dev/null | grep -i 'build date'
#    Su CloudLinux/LiteSpeed, i binari per versione:
ls -l --time-style=full-iso /opt/alt/php8*/usr/bin/php-cgi

# 4. Nei container, dentro il container
docker exec <container> php -i | grep -i 'build date'

# 5. Riavvio, poi riverifica
systemctl restart php8.3-fpm apache2

Per l’esposizione su un parco macchine grande, Searchlight Cyber ha pubblicato php2shell-scanner, open source, che verifica da fuori. Utile per l’inventario, ma non sostituisce il controllo sulla macchina: dice che un host risponde in un certo modo, non quando hai ricostruito i binari.

Sugli indicatori di compromissione, quelli citati dai vendor e che si cercano nei log: richieste multipart anomalamente grandi o con un numero eccessivo di campi, parametri di sessione strani, valori PHPSESSID insolitamente grandi negli header Set-Cookie, errori di memoria esaurita di PHP e crash ricorrenti dei worker da correlare col traffico. Poi i segnali di compromissione veri: processi figli inattesi, file applicativi modificati, connessioni in uscita insolite.

Un punto che va detto senza addolcirlo: alcune tecniche dimostrate includono la lettura non autenticata della memoria dei worker, dove passano password, token e chiavi API. Se un tuo server è stato esposto a sfruttamento attivo, patchare chiude la porta ma non dice cosa è già uscito. Le credenziali che quel processo aveva in memoria vanno considerate da ruotare.

La lezione vera: l’inventario va fatto prima

Se c’è una cosa che questa vulnerabilità insegna è che il lavoro difficile non è patchare, è sapere cosa devi patchare.

Chi gestisce più di una manciata di siti li ha sparsi su hoster diversi, con SAPI diversi, alcuni in container, alcuni su pannelli che decidono le versioni al posto tuo. Quando esce una RCE nel runtime con l’exploit già in circolazione, il tempo non se ne va nell’applicare la correzione: se ne va nel ricostruire l’elenco di cosa esiste e dove, mentre l’orologio gira.

Quell’elenco va costruito in un giorno qualsiasi, non il giorno della CVE. E va costruito con i campi giusti: non solo “versione di PHP”, ma hoster, SAPI in uso, data di build dei binari e canale da cui arrivano gli aggiornamenti. È esattamente la differenza tra sapere di essere a posto e sperarlo.

Secondo aspetto, meno ovvio: un inventario che contiene solo la versione ti avrebbe dato, su un server CloudLinux patchato, la stessa risposta di un server vulnerabile. Se il campo che registri è quello sbagliato, l’inventario ti fa sentire più sicuro senza renderti più sicuro, che è il peggiore dei due mondi.

Il conto della superficie d’attacco

Chiudo con l’osservazione che mi interessa di più, e provo a farla senza vendere niente.

Questo sito non ha richiesto nessuna di queste verifiche. È Astro, HTML statico su Cloudflare Pages: non c’è PHP, non c’è un worker che resta in memoria tra due richieste, non c’è un runtime da patchare. Quando esce una RCE nel linguaggio, il lavoro è zero perché la superficie è zero.

Non lo scrivo come superiorità tecnica, perché sarebbe sciocco: su quello stack non si costruisce un e-commerce con quindici integrazioni, e metà dei siti che gestiamo in _blank sono su WordPress per ragioni che restano valide anche oggi. Lo scrivo perché quando si confrontano gli stack si parla sempre di performance, costi mensili e autonomia del cliente, e quasi mai della voce arrivata il 21 ottobre: le giornate in cui una CVE nel runtime obbliga a toccare ogni server che hai, verificare date di build e ricostruire immagini Docker. È una voce reale del costo totale, e va messa nel conto quando si sceglie, non quando arriva il conto.

Ne avevo scritto quando ho scelto lo stack di questo blog, e la riflessione di allora regge: Astro, Ghost o WordPress per un blog tecnico.

Quel post però si fermava a un blog personale, cioè al caso più facile che esista: un solo autore, che è anche quello che tocca il codice. Questo sito è servito da esperimento, ed è andato abbastanza bene da farci decidere sul serio: in _blank abbiamo poi portato su Astro anche i dieci anni di sito dell’agenzia, tenendo però un CMS headless davanti per chi scrive. È lì che si risolve l’obiezione seria allo statico, cioè che senza terminale non pubblichi: non negandola, ma rimettendo un pannello sopra i contenuti e lasciando statico quello che arriva al browser.

Se gestisci server PHP e non l’hai ancora fatto, smetti di leggere e vai a controllare le date di build.

Domande frequenti

Come faccio a sapere se il mio server è patchato contro PHP2Shell?

Il numero di versione non basta: su un hosting CloudLinux la versione è rimasta 8.5.11 sia prima sia dopo la correzione del 21 ottobre 2026, per poi passare a 8.5.12 il giorno dopo. Il dato che conta è la data di build del binario (php -i | grep -i 'build date'), da controllare sul SAPI che serve il web e non solo sulla CLI, dentro il container se usi Docker. Nel caso descritto qui la CLI passava da “built Oct 6” a una build del 21 ottobre, senza cambio di versione. Dopo l'aggiornamento serve anche il riavvio dei worker.

Quali versioni di PHP correggono PHP2Shell?

Le versioni corrette sono 8.5.12, 8.4.27, 8.3.36 e 8.2.35, come indica l'avviso del PHP Security Team del 21 ottobre 2026. Per 7.4, 8.0 e 8.1 il team ha pubblicato backport minimi di sorgente, pensati per chi compila in proprio. Chi usa una distribuzione o un hoster con backport può avere una versione che sembra vecchia ma è già corretta: in quel caso va verificata la data di build, non il numero.

PHP2Shell colpisce solo WordPress?

No. Il difetto sta nel linguaggio e non nell'applicazione, quindi riguarda qualsiasi installazione PHP che serva richieste web: PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP e LiteSpeed. L'avviso ufficiale indica il rischio più alto per mod_php, con un exploit dimostrato in cui una richiesta non autenticata fa scattare l'attacco prima che giri qualsiasi codice dell'applicazione. Un sito scritto bene è esposto come uno scritto male.

Una regola WAF basta a proteggersi da PHP2Shell?

No: le regole WAF sono un controllo compensativo, mitigano i pattern noti ma non correggono il difetto. L'avviso ufficiale dice anche che le mitigazioni di configurazione riducono l'esposizione a percorsi specifici ma “do not make an installation safe”. In più vanno riviste le regole di skip scritte in passato: su una delle mie zone una regola che disattivava il WAF sugli upload della webmail apriva una scorciatoia proprio dove passano tre delle cinque tecniche di sfruttamento.

PHP2Shell è già sfruttata attivamente?

Al 22 ottobre 2026 le fonti non concordano. watchTowr dichiara di osservare tentativi di sfruttamento attivi di entrambe le vulnerabilità, mentre Rapid7 non ha osservato sfruttamento in the wild ma riporta più segnalazioni di tentativi e giudica i proof of concept pubblici non affidabili in generale. Amazon ha dichiarato un tentativo già il 17 ottobre, quattro giorni prima della divulgazione. In sintesi: tentativi sì, exploit universale e affidabile non ancora, ed è un motivo per muoversi subito.

Dopo l'aggiornamento di PHP devo riavviare qualcosa?

Sì. Aggiornare i pacchetti non sostituisce il codice già in memoria nei processi in esecuzione: finché non riavvii, i worker continuano a eseguire il PHP vulnerabile anche se il filesystem risulta aggiornato. Vanno riavviati Apache, PHP-FPM e ogni altro processo che esegue PHP, container compresi. Nel caso Docker il comando up -d che ricrea il container fa già il lavoro, ma il riavvio va poi verificato.