Vulnerabilità php2shell: protezione dei server di gioco con il runbook di patch in 6 ore
In breve
Scopri come proteggere i server di gioco dalla vulnerabilità php2shell con questo runbook di patch in 6 ore: inventario, fix e rilevamento rapido.
Il livello PHP del tuo gioco è probabilmente la parte dello stack che nessuno guarda. Lo store web che vende cosmetici. Il pannello admin che usa il community manager. L'endpoint legacy per le leaderboard che hai scritto nel 2022 e non hai mai più toccato. È questa la superficie che php2shell attacca.
Il 21 ottobre 2026, Calif.io, Wiz, OpenAI e Quarkslab hanno divulgato due vulnerabilità del runtime PHP tracciate come CVE-2026-109618 e CVE-2026-109619, denominate collettivamente php2shell. Il loro punteggio CVSS v4.0 è 9.2. Colpiscono ogni installazione PHP dalla 5.4 in poi, consentono esecuzione remota di codice non autenticata, esposizione di dati e denial of service, e Cloudflare ha osservato traffico di attacco live che mutava i payload contro clienti reali prima ancora che la divulgazione pubblica avvenisse.
Non è un problema di plugin WordPress. È un bug di memory safety nel runtime PHP stesso. Aggiornare il framework, il CMS o le dipendenze Composer non serve a nulla. Se gestisci un backend di gioco che contiene PHP, questo runbook fa per te.
Cosa rompe realmente php2shell
Entrambe le CVE risiedono nel modo in cui PHP gestisce la memoria quando l'elaborazione delle richieste va storta. Normalmente PHP alloca memoria per una richiesta, la usa e la rilascia quando la richiesta termina. I bug compaiono quando il percorso di cleanup opera su uno stato che non è mai stato valido.
CVE-2026-109618: corruzione out-of-memory
PHP raggiunge il tetto di memoria a metà richiesta e si interrompe prima che alcune strutture interne finiscano di inizializzarsi. La routine di cleanup tratta quindi quei dati a metà costruzione come legittimi. Un attaccante che riesce a forzare in modo affidabile condizioni di OOM — body di richiesta grandi, input profondamente annidati, upload sovradimensionati — ha la possibilità di controllare ciò che contiene quella memoria incompleta.
Per i backend di gioco il trigger pratico è ovvio: qualsiasi endpoint che accetta un body senza limite di dimensione. Invio dei risultati delle partite, upload di screenshot, metadati delle mod, batch di telemetria.
CVE-2026-109619: stato globale stale tra le richieste
Un errore fatale interrompe una richiesta, lasciando il processo PHP con riferimenti a memoria che viene liberata alla fine della richiesta. Quando lo stesso processo worker gestisce la successiva richiesta, può seguire quei riferimenti dangling. Questa è esposizione di dati: i dati di sessione o le credenziali del database di un giocatore che finiscono nella risposta di un altro giocatore — e, nelle condizioni giuste, esecuzione di codice.
Questo è il bug più insidioso per i server di gioco perché i worker PHP-FPM sono long-lived by design. Un singolo worker avvelenato può servire centinaia di richieste successive prima di essere riciclato. Se usi pm.max_requests = 0 (il default, che significa mai riciclare), un worker corrotto può persistere per l'intera vita del container.
Step 1: Fai l'inventario di ogni runtime PHP che possiedi
Non puoi patchare ciò che non hai trovato. Molti studi scoprono, a metà incidente, che tre dei loro servizi usano PHP e che uno di questi è su 8.1 con un'immagine base costruita due anni fa. Esegui questo prima di fare qualsiasi altra cosa.
#!/usr/bin/env bash
# php2shell exposure inventory (CVE-2026-109618 / CVE-2026-109619)
set -euo pipefail
declare -A FIXED=( [8.5]="8.5.12" [8.4]="8.4.27" [8.3]="8.3.36" [8.2]="8.2.35" )
SERVICES=("game-api" "web-store" "admin-panel" "forums")
for svc in "${SERVICES[@]}"; do
ver=$(docker compose exec -T "$svc" php -r 'echo PHP_VERSION;' 2>/dev/null | tr -d '[:space:]') || {
echo "SKIP $svc (no php binary)"; continue; }
branch="${ver%.*}" # "8.3.12" -> "8.3"
min="${FIXED[$branch]:-}"
if [[ -z "$min" ]]; then
echo "EOL $svc on $ver -> no upstream patch, upgrade required"
elif php -r 'exit(version_compare($argv[1], $argv[2], ">=") ? 0 : 1);' "$ver" "$min"; then
echo "OK $svc on $ver"
else
echo "VULN $svc on $ver (fixed in $min)"
fi
done
Due cose che questo script fa volutamente: segnala i branch EOL separatamente e non assume mai che php -v sull'host rifletta il runtime reale del container. Se sei su Kubernetes, sostituisci docker compose exec con kubectl exec e itera sui deployment invece che sui nomi dei servizi.
Le versioni upstream corrette sono 8.5.12, 8.4.27, 8.3.36 e 8.2.35. Qualsiasi cosa su 8.1, 8.0, 7.x o 5.x è end-of-life e non riceverà mai una fix. Devi fare un upgrade, non una patch.
Step 2: Patcha il runtime, non l'applicazione
La modalità di errore più comune nella prima ora di un incidente del genere è un team che aggiorna il codice applicativo, fa redeploy, vede CI verde e pensa di aver finito. Non è così.
- PHP-FPM e mod_php: ricostruisci l'immagine base o aggiorna il pacchetto. Debian/Ubuntu:
apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine:apk upgrade php83. Verifica conphp -r 'echo PHP_VERSION;'dentro il container in esecuzione, non sull'host di build. - FrankenPHP / php-cgi: questi includono il runtime, quindi ti serve un nuovo binario di release o una nuova immagine base, non un semplice aggiornamento del pacchetto.
- Hosting gestito: se il provider controlla PHP, apri subito un ticket e chiedi il riferimento all'advisory. Non accettare "siamo a conoscenza" come risoluzione.
- Immagini base Docker: fissa il digest, non il tag.
php:8.3-fpmpuò risolvere a una build diversa al prossimo pull, ottimo per le patch e terribile per la riproducibilità. Fissa il digest dopo aver verificato la versione.
Dopo la patch, uccidi ogni worker esistente. Un rolling restart che lascia vivi i vecchi processi su una socket condivisa mantiene i worker vulnerabili a servire traffico. docker compose restart o kubectl rollout restart — verifica che l'orario di avvio del processo sia cambiato.
Step 3: Riduci il raggio d'esplosione mentre aspetti
Patchare richiede tempo. Hai una finestra tra "lo sappiamo" e "ogni ambiente è stato ricostruito". Usala.
Limita i body delle richieste al perimetro
# Shrink the php2shell trigger surface
client_max_body_size 2m; # game APIs rarely need more than this
client_body_timeout 10s;
client_header_timeout 10s;
large_client_header_buffers 4 8k;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php-fpm:9000;
fastcgi_read_timeout 15s;
fastcgi_buffering on;
}
Un limite di 2 MB non ferma un attaccante determinato — può innescare OOM con molte richieste piccole — ma elimina il banale trigger a singola richiesta e non costa nulla.
Ricicla i worker in modo aggressivo
; /usr/local/etc/php-fpm.d/zz-php2shell.conf
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 20
pm.max_requests = 500
pm.max_children = 24
request_terminate_timeout = 20s
Ecco la matematica del trade-off. A 200 richieste al secondo su 24 worker, ogni worker gestisce circa 8,3 req/s. Con pm.max_requests = 500, ogni worker viene riciclato ogni ~60 secondi. Questo limita per quanto tempo un processo corrotto da CVE-2026-109619 può servire traffico, ma significa anche 24 spawn di processi al minuto — overhead misurabile su framework con cold start pesante.
Alzare memory_limit da 128M a 256M riduce la frequenza con cui si arriva a OOM (meno trigger per CVE-2026-109618) ma raddoppia la memoria nel caso peggiore: 24 figli × 256M = 6 GB di margine che ora servono sul nodo. Se il nodo ha 4 GB, abbassa pm.max_children invece di fingere che quella memoria esista.
Niente di tutto questo è una fix. Ti compra ore. Trattalo come triage.
Step 4: Rileva se sei già stato colpito
La divulgazione non equivale alla prima exploitazione. Il traffico di attacco è stato osservato prima della data di rilascio ufficiale. Parti dal presupposto che tu sia stato sondato.
# 1. OOM aborts — strongest single signal for CVE-2026-109618
grep -c "Allowed memory size" /var/log/php-fpm/error.log
# 2. Worker crashes — signal for both CVEs
journalctl -u php8.3-fpm --since "48 hours ago" \
| grep -Ei "segfault|SIGSEGV|exited on signal 11"
# 3. Which paths are generating 500s
awk '$9 == 500 {print $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
# 4. Same source IP hitting the same 500-ing path repeatedly
awk '$9 == 500 {print $1, $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
Prima la baseline. Se il log PHP-FPM aveva già 40 abort OOM al giorno da un endpoint di export che consuma molta memoria, quel numero è rumore. Quello che cerchi è un cambiamento nel tasso: un picco di uscite con signal 11, un nuovo path che compare nella lista dei 500, o un singolo IP che genera decine di errori contro un endpoint.
Se trovi prove di exploit riuscito, trattalo come una violazione completa: ruota le credenziali del database, le chiavi di firma delle sessioni, i segreti del provider di pagamento e qualsiasi API key raggiungibile dal processo PHP. Il playbook per questo non è specifico di PHP — la stessa disciplina che permette a uno studio di architettare backend di gioco che sopravvivano alle compromissioni vale anche qui, e parte dal presupposto che l'attaccante abbia avuto esecuzione di codice, non solo accesso in lettura.
Se il tuo backend sta già scartando richieste e non sei sicuro se sia carico o attacco, l'ordine di triage dei crash descritto in questo protocollo diagnostico per i freeze del server vale anche qui: isola il tier che fallisce prima di toccare la configurazione.
Perché le regole WAF basate solo sulla richiesta hanno fallito
Il report di Cloudflare contiene la lezione di engineering più trasferibile dell'intero incidente. Le loro protezioni iniziali erano basate sulla richiesta, e gli attaccanti hanno risposto mutando i payload — sondando quali varianti corrispondevano e quali passavano, usando il segnale match/no-match come oracolo.
I soli dati della richiesta non potevano separare il traffico di exploit da quello legittimo, perché una grande POST con array annidati è anche ciò che un normale client di gioco invia quando presenta un riepilogo della partita. Il segnale che ha risolto l'ambiguità è stata la risposta: l'origine ha restituito un 500 con una traccia di errore fatale PHP? Quella combinazione — richiesta sospetta più risposta diagnostica — ha alta confidenza. La richiesta da sola no.
Cloudflare ha rilasciato questa funzionalità come "full transaction detection," correlando richiesta e risposta all'interno della stessa transazione HTTP e bloccando la risposta prima che raggiunga il richiedente. Se gestisci il tuo edge o reverse proxy, il pattern merita di essere copiato:
def is_probable_php2shell(resp) -> bool:
"""Correlate request shape with origin response, not request alone."""
body = resp.body[:4096]
return (
resp.status == 500
and b"Fatal error" in body
and resp.origin_latency_ms < 50 # exploit aborts fast
and b"Allowed memory size" in body # OOM path = 109618 signature
)
La lezione va oltre questa CVE: qualsiasi sistema di rilevamento che vede solo il traffico in ingresso verrà bypassato da un attaccante che può vedere il risultato in uscita. Registra le risposte. Correlale. È la differenza tra una regola che regge e una regola che diventa un puzzle.
Buone pratiche
- Patcha secondo una pianificazione, non alla divulgazione. Il divario tra "CVE pubblicata" e "exploit in the wild" ora si misura in ore, a volte negativo. Ricostruisci le immagini base PHP ogni settimana anche quando non viene annunciato nulla e tieni un percorso di rollback.
- Non eseguire mai
pm.max_requests = 0in produzione. Una durata illimitata dei worker trasforma ogni bug di memory safety in un punto d'appoggio persistente. 500–1000 è un default ragionevole per le API di gioco. - Limita il body di ogni richiesta al perimetro. 2 MB per le API JSON, 10 MB per gli upload, e rifiuta qualsiasi cosa senza un content type corrispondente. Questo elimina intere classi di exploit legati alla pressione sulla memoria prima che arrivino a PHP.
- Registra i body delle risposte per gli errori 5xx, in modo campionato. Non puoi costruire una full-transaction detection se le risposte dell'origine sono invisibili alla tua stack di osservabilità. Campiona il 10% e conserva per 7 giorni.
- Mantieni un inventario di ogni runtime che gestisci. Non solo PHP — Node, Python, Go, qualunque cosa esegua il tuo web tier. Gli studi che hanno risposto a php2shell in meno di un'ora avevano un catalogo dei servizi. Quelli che hanno impiegato un giorno stavano facendo grep nei Dockerfile.
La lezione più grande: il tuo web tier è la superficie d'attacco
php2shell è un promemoria del fatto che i backend di gioco accumulano cruft. Al primo anno pubblichi un pannello admin PHP perché è veloce da costruire. Al terzo anno è ancora lì, ancora su 8.1, ancora con una credenziale del database con accesso in scrittura alla tabella dei giocatori — ed è quello che viene bucato, non il tuo binario di game server indurito.
La fix strutturale è ridurre quanto runtime possiedi. Se l'unico motivo per cui gestisci un web tier PHP è occuparti di account dei giocatori, cloud save e leaderboard, quelle sono esattamente le funzionalità che horizOn espone come API gestite — autenticazione dell'utente finale con sessioni legate all'account, Cloud Save con letture e scritture consapevoli delle revisioni, dove il client risolve i conflitti, leaderboard e configurazione remota. Nessun pool PHP-FPM da patchare alle 3 di notte, perché non esiste alcun pool PHP-FPM.
Non è un argomento per dire che i servizi gestiti sono immuni alle vulnerabilità. È un argomento per dire che ogni runtime che gestisci è un runtime che devi inventariare, patchare e monitorare — e che la maggior parte degli studi ne ha più di quanto pensi.
Fai questo ora
Se c'è una sola azione da portarti via da questo articolo, fai in modo che sia lo script di inventario dello Step 1. Eseguilo su ogni ambiente — produzione, staging, il server demo dimenticato di quella presentazione con l'editore. Quasi certamente troverai un runtime di cui ti eri dimenticato, e quasi certamente sarà su un branch EOL.
Poi patch, ricicla i worker e lascia attive le regole al perimetro. Le regole WAF e i limiti sui body sono difesa in profondità, non un sostituto della fix del vendor — l'advisory PHP è la fonte di verità ed è ciò che chiude davvero il buco.
Se l'inventario fa emergere più PHP di quanto tu voglia gestire d'ora in poi, horizOn copre il tier di account, save e leaderboard come servizi gestiti, ed è il modo più rapido per ridurre la superficie di cui sei responsabile per le patch. In ogni caso: prima l'inventario, poi la patch, e smetti di dare per scontato che la parte noiosa della tua stack sia quella sicura.
Fonte: Cloudflare implementa protezioni contro la vulnerabilità php2shell