Cosa è successo
Il 20 ottobre il team di sicurezza di PHP ha preannunciato una release di sicurezza per il giorno dopo, parlando di due vulnerabilità critiche con CVSS v4.0 di 9.2. Il 21 ottobre alle 14:00 UTC sono uscite le versioni corrette:
- PHP 8.5.12
- PHP 8.4.27
- PHP 8.3.36
- PHP 8.2.35
Il messaggio ufficiale è chiaro: aggiornare subito. Sono due esecuzioni di codice remoto senza autenticazione, ribattezzate "PHP2Shell", e secondo ISPConfig ci sono già attacchi in circolazione.
Il punto debole è il codice di PHP che interpreta le richieste multipart/form-data, il formato che i browser usano per caricare file. E qui sta la parte subdola: PHP fa il parsing di una richiesta multipart prima di eseguire lo script. Quindi non serve avere un form di upload per essere vulnerabili: basta una singola richiesta costruita apposta verso qualsiasi file PHP. Non importa quanto sia scritto bene il tuo WordPress, il tuo Laravel o il tuo gestionale custom: se l'interprete è vulnerabile, sei esposto. Vale anche per phpMyAdmin, Roundcube e per il pannello stesso (ISPConfig incluso).
Le due vulnerabilità in parole semplici
CVE-2026-109618 · RCE tramite corruzione in caso di out-of-memory
Quando PHP esaurisce la memoria nel momento sbagliato, cioè mentre sta ancora inizializzando i dati interni della richiesta, la fase di pulizia finale può trattare quei dati parziali come se fossero validi. Un attaccante che riesce a provocare un OOM nel punto giusto (per esempio con una richiesta costruita apposta) può sfruttare questa incoerenza.
CVE-2026-109619 · RCE tramite stato globale "stantio"
Alcuni dati interni legati a una richiesta possono sopravvivere dopo che la memoria che li conteneva è stata liberata. La richiesta successiva gestita dallo stesso worker può usare quello stato prima che venga reinizializzato. In un ambiente come PHP-FPM, dove i worker servono molte richieste di fila, questo è il classico scenario da use-after-free.
Chi è a rischio
Tutte le installazioni PHP che servono richieste web: PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP, LiteSpeed. Il rischio più alto è su mod_php, per il quale esiste già un exploit dimostrato che parte da una richiesta non autenticata.
Se hai ancora qualche vecchio vhost con mod_php, questo è il momento giusto per passare a PHP-FPM.
Il mio giro di verifiche sui server dei clienti
Ieri ho passato la giornata a controllare i server che gestisco direttamente e quelli dei clienti su hosting di terzi. Il quadro è stato piuttosto variegato:
| Ambiente | Stato |
|---|---|
| Server con ISPConfig (gestiti da me) | Aggiornati in automatico via cron, nessun intervento |
| cPanel | Tutto aggiornato |
| Plesk | Tutto aggiornato |
| Shopware su AWS | Aggiornato da solo senza problemi |
| Server gestiti da Register (pannello wepanel) | Nessuno aggiornato al momento del controllo |
ISPConfig ha inoltre pubblicato uno strumento di mitigazione, ispconfig-multipart-guard, pensato per coprire il buco tra l'annuncio e l'arrivo dei pacchetti aggiornati, e soprattutto per le versioni di PHP custom o fuori supporto che una patch potrebbero non riceverla mai. Ne parlo più sotto.
La lezione è sempre la stessa: gli aggiornamenti automatici di sicurezza salvano la giornata. Dove c'erano, non ho dovuto fare niente. Dove l'hosting è gestito da altri e non hai accesso root, l'unica cosa che puoi fare è aprire un ticket e, nel frattempo, applicare le mitigazioni che hai a disposizione (vedi sotto).
Come verificare che la patch sia davvero attiva
Qui c'è un tranello in cui è facile cadere: il pacchetto aggiornato non basta, serve riavviare i worker.
1. Controlla la versione che sta servendo il web, non quella della CLI
php -v ti dice la versione della CLI, che può essere diversa da quella usata da FPM. Controlla il binario di FPM:
php-fpm8.3 -v
# oppure, su sistemi con un solo FPM
php-fpm -v
Oppure crea temporaneamente un file con phpinfo() dentro il vhost, controlla e cancellalo subito.
2. Sui pacchetti delle distribuzioni la versione può non cambiare
Debian, Ubuntu e RHEL spesso applicano la patch con un backport mantenendo lo stesso numero di versione upstream. In quel caso cerca il CVE nel changelog del pacchetto:
# Debian / Ubuntu
apt changelog php8.3-fpm | grep -E "CVE-2026-10961[89]"
# RHEL / AlmaLinux / Rocky
rpm -q --changelog php-fpm | grep -E "CVE-2026-10961[89]"
Se il CVE compare, il pacchetto è patchato.
3. Riavvia i worker
Un PHP-FPM aggiornato ma mai riavviato continua a girare con il codice vecchio in memoria:
systemctl restart php8.3-fpm
# con mod_php
systemctl restart apache2
Se usi unattended-upgrades, installa anche needrestart: dopo l'aggiornamento ti segnala (o riavvia in automatico) i servizi che stanno ancora usando librerie vecchie.
4. PHP 8.1 e precedenti
Il progetto PHP non rilascia più aggiornamenti per 8.1 e versioni più vecchie. Vista la gravità, però, il team di sicurezza ha pubblicato dei backport minimi in sorgente per PHP 7.4, 8.0 e 8.1 su GitHub, utili a chi compila da sé o non ha un vendor LTS. Se invece hai i pacchetti di una distro LTS, controlla se il vendor ha fatto il backport.
E ricorda: PHP 8.2 arriva a fine vita il 31 dicembre 2026. Se hai siti su 8.2, pianifica il passaggio adesso.
Se non puoi aggiornare subito: le mitigazioni
Premessa importante, ripetuta anche nell'avviso ufficiale: nessuna di queste misure rende sicura un'installazione. Riducono l'esposizione a percorsi di attacco specifici e ti fanno guadagnare tempo. L'unica soluzione completa è l'aggiornamento.
La mitigazione più efficace: bloccare il multipart nel web server
Se il problema sta nel parser multipart e il parser gira prima del tuo codice, la difesa più affidabile è che quelle richieste non arrivino proprio a PHP. Il prezzo da pagare è chiaro: finché il blocco è attivo, gli upload di file non funzionano (media library del CMS, form con allegati, allegati della webmail, import SQL di phpMyAdmin). Le normali pagine, i login e i form senza file continuano a funzionare, perché usano il formato urlencoded.
Su ISPConfig: ispconfig-multipart-guard
ISPConfig ha pubblicato uno script che fa esattamente questo, su nginx e su Apache 2.4, per tutto il server o solo per alcuni siti. Requisiti: Debian o Ubuntu con ISPConfig 3.2 o 3.3 (funziona anche su server senza ISPConfig), systemd e accesso root.
wget -O /usr/local/sbin/ispconfig-multipart-guard https://www.ispconfig.org/downloads/ispconfig-multipart-guard.sh
chmod 755 /usr/local/sbin/ispconfig-multipart-guard
ispconfig-multipart-guard install
L'installazione aggiunge gli hook al web server, blocca subito il vhost del pannello (che di upload non ha bisogno), crea un cron che ogni 5 minuti ripristina gli hook se un aggiornamento li ha rimossi e lancia un self-test. I siti però non vengono bloccati finché non lo chiedi tu:
ispconfig-multipart-guard block all # tutti i vhost del server
ispconfig-multipart-guard block example.com # solo un sito
ispconfig-multipart-guard sites # stato di ogni vhost
ispconfig-multipart-guard test # verifica che il blocco funzioni
Qualche dettaglio utile:
- Allowlist: in
/etc/ispconfig-multipart-guard/allowlistpuoi mettere l'IP del tuo ufficio per continuare a caricare file mentre il blocco è attivo (poiispconfig-multipart-guard repair). Se sei dietro Cloudflare o un altro proxy, prima ripristina il vero IP del client (realipsu nginx,mod_remoteipsu Apache) e non mettere mai in allowlist i range della CDN, o apri gli upload a tutti. - Su nginx le richieste bloccate ricevono un 415, su Apache un 403. Su Apache la regola viene applicata dopo
.htaccesse direttive del sito, quindi un sito non può riaprirsi da solo il varco. - Multiserver: lo script va eseguito su ogni web server.
- Server con Apache 2.2: c'è uno script dedicato, ma blocca tutti i vhost senza selezione per sito. Quei sistemi non ricevono più aggiornamenti di PHP da anni: vanno migrati.
Quando il PHP aggiornato è installato e i servizi FPM riavviati, si spegne tutto con:
ispconfig-multipart-guard off # tiene gli hook per la prossima emergenza
Per verificare dall'esterno che un sito sia bloccato:
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: multipart/form-data; boundary=x' \
--data-binary $'--x--\r\n' https://example.com/index.php
# 415 (nginx) o 403 (Apache) = bloccato
Senza ISPConfig: la stessa idea a mano
Su nginx, nel contesto http:
map $http_content_type $block_multipart {
default 0;
~*multipart 1;
}
e nella location che passa le richieste a PHP-FPM:
if ($block_multipart) {
return 415 "Upload temporaneamente disabilitati";
}
Su Apache 2.4, in una configurazione globale (per esempio conf-available/block-multipart.conf abilitata con a2enconf), con eccezione per un IP di fiducia:
<If "%{HTTP:Content-Type} =~ /multipart/i && ! -R '203.0.113.10'">
Require all denied
</If>
Ricarica il web server (nginx -t && systemctl reload nginx, oppure apache2ctl -t && systemctl reload apache2) e verifica con il curl di sopra.
Attenzione a ini_set() e .user.ini
Questo è il punto su cui sbagliare è più facile. Parte dell'attacco avviene durante il parsing della richiesta, cioè prima che il tuo script venga eseguito. Un ini_set() nel codice arriva troppo tardi, e anche il file .user.ini di FPM viene esplicitamente indicato come non affidabile per queste mitigazioni.
Le direttive vanno messe dove vengono lette prima della richiesta:
php.ini- configurazione del pool FPM (
php_admin_value/php_admin_flag) - configurazione di Apache (
php_admin_valuenel vhost, per mod_php)
Usare php_admin_value invece di php_value ha un vantaggio in più: l'applicazione non può sovrascrivere il valore.
Mitigazioni per CVE-2026-109618 (OOM)
1. memory_limit = -1, in modo non sovrascrivibile. Sembra controintuitivo, ma il punto è eliminare il fallimento controllabile del memory limit di PHP, che è proprio quello che l'attaccante sfrutta. Verifica che nessun pool, directory o impostazione applicativa rimetta un limite finito. Un fallimento di allocazione a livello di sistema può comunque raggiungere il codice vulnerabile, quindi è una riduzione, non una chiusura.
2. post_max_size basso. 8M se l'applicazione non riceve upload, 32M se li riceve. upload_max_filesize deve restare sotto post_max_size.
3. Un limite di memoria esterno al posto del memory_limit. Togliendo il memory_limit, uno script impazzito può mangiarsi tutta la RAM. Il consiglio ufficiale è usare un limite di container o cgroup, possibilmente con una policy che uccida solo il worker coinvolto. Con systemd:
systemctl edit php8.3-fpm
[Service]
MemoryMax=2G
OOMPolicy=continue
OOMPolicy=continue evita che systemd fermi l'intero servizio quando il kernel uccide un singolo worker. Non usare invece RLIMIT_AS o RLIMIT_DATA (cioè ulimit -v o simili): possono provocare proprio il tipo di fallimento di allocazione che apre la vulnerabilità.
4. Limiti sulla richiesta a livello di web server o proxy. Dimensione del body, numero di campi, numero di parti multipart, numero di header, profondità di annidamento. Questi limiti devono valere anche per le richieste senza un Content-Length utilizzabile (per esempio quelle in chunked encoding).
Nginx:
client_max_body_size 32m;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
Apache:
LimitRequestBody 33554432
LimitRequestFields 50
LimitRequestFieldSize 8190
LimitRequestLine 8190
Se hai un WAF davanti (ModSecurity, Cloudflare, un reverse proxy), è il posto giusto per limitare anche il numero di parti nei form multipart/form-data.
Mitigazioni per CVE-2026-109619 (stato globale)
1. Disabilita l'upload progress delle sessioni. session.upload_progress.enabled = 0 e riavvia tutti i worker. Va messo in php.ini o nel pool FPM, non con ini_set() né in .user.ini. Blocca i percorsi confermati che passano dalle sessioni, ma non tutte le varianti: anche ISPConfig sottolinea che da sola non basta, proprio perché il parser multipart gira prima di qualsiasi gestione delle sessioni. Usala insieme al blocco nel web server, non al suo posto. Nella pratica pochissime applicazioni usano davvero questa funzione, quindi disattivarla non costa quasi nulla.
2. Su PHP 8.5: fatal_error_backtraces = 0 e riavvio dei worker. Blocca solo il percorso confermato che passa dai backtrace degli errori fatali.
3. Ripulisci le impostazioni invalide o deprecate applicate per richiesta. Se un php_value in un .htaccess o in un pool emette avvisi prima che la richiesta sia inizializzata, quell'avviso è un vettore. Nascondere i messaggi con error_reporting, display_errors o @ non serve: il problema è che l'avviso viene generato, non che venga mostrato. Un modo rapido per scovarli è cercare nei log gli avvisi senza file di origine:
grep -E "PHP (Warning|Deprecated).*Unknown on line 0" /var/log/php*/*.log
4. Sistema gli errori fatali e i timeout raggiungibili dall'esterno. Inclusi quelli nei callback, nelle procedure di upload e import e nelle routine di pulizia. Se un utente anonimo può far andare in fatal error o in timeout uno script, quello è un punto da chiudere. Qui torna utile avere un monitor sull'error log: ne ho parlato in questo articolo sul bot Telegram.
Esempio di pool FPM con le mitigazioni
; /etc/php/8.3/fpm/pool.d/www.conf (o il pool del sito)
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
php_admin_flag[session.upload_progress.enabled] = off
; solo PHP 8.5
; php_admin_flag[fatal_error_backtraces] = off
Su ISPConfig le stesse righe si possono inserire per singolo sito nel campo delle impostazioni php.ini personalizzate (scheda Opzioni del sito web). Su cPanel e Plesk passano dai rispettivi editor delle impostazioni PHP; se l'editor non permette valori non sovrascrivibili, meglio chiedere al supporto.
Hosting gestiti: cosa chiedere al provider
Se il sito è su un hosting dove non hai root, il ticket deve essere specifico, altrimenti rischi una risposta generica. Qualcosa del genere:
Buongiorno, in riferimento all'avviso di sicurezza PHP del 21/10/2026 (CVE-2026-109618 e CVE-2026-109619, CVSS 9.2), vi chiedo di confermare che la versione PHP in uso sul mio piano sia stata aggiornata a 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (o a un pacchetto con backport delle due CVE) e che i processi PHP siano stati riavviati. In caso contrario vi chiedo una data prevista per l'aggiornamento.
Nel frattempo applica quello che il pannello ti consente: post_max_size basso, disattivazione dell'upload progress delle sessioni (se il pannello scrive nel pool o nel php.ini e non in un .user.ini).
Checklist finale
- Versione FPM / mod_php aggiornata (o CVE presente nel changelog del pacchetto)
- Worker PHP riavviati
- Nessun vhost rimasto su mod_php
- Siti su PHP 8.1 o precedenti: backport applicato o migrazione pianificata
- Siti su PHP 8.2: migrazione pianificata entro fine anno
- Dove non si può aggiornare: blocco delle richieste multipart nel web server (su ISPConfig con ispconfig-multipart-guard)
- Mitigazioni aggiuntive nel pool / php.ini, non con
ini_set() - Dopo l'aggiornamento: blocco multipart disattivato e upload di nuovo funzionanti
- Ticket aperti verso gli hosting gestiti che non risultano aggiornati
- Aggiornamenti automatici di sicurezza attivi dove ancora mancano
