php2shell-Sicherheitslücke: Game-Server-Schutz – Das 6-Stunden-Patch-Runbook
Kurz und knapp
Schütze dein Game-Backend vor php2shell: Inventar, Patch, Worker-Recycling und Edge-Regeln – das schnelle 6-Stunden-Runbook für CVE-2026-109618/619.
Die PHP-Ebene deines Spiels ist wahrscheinlich der Teil deines Stacks, den niemand beachtet. Der Web-Store, der Kosmetikartikel verkauft. Das Admin-Panel, das dein Community-Manager nutzt. Der Legacy-Leaderboard-Endpoint, den du 2022 geschrieben und nie wieder angefasst hast. Das ist die Angriffsfläche, die php2shell ausnutzt.
Am 21. Oktober 2026 haben Calif.io, Wiz, OpenAI und Quarkslab zwei PHP-Runtime-Sicherheitslücken offengelegt, die als CVE-2026-109618 und CVE-2026-109619 verfolgt und gemeinsam als php2shell bezeichnet werden. Der CVSS-v4.0-Score liegt bei 9.2. Sie betreffen jede PHP-Installation ab Version 5.4, ermöglichen nicht authentifizierte Remote Code Execution, Datenoffenlegung und Denial-of-Service – und Cloudflare beobachtete bereits vor der öffentlichen Offenlegung Live-Angriffstraffic, der Payloads gegen echte Kunden mutierte.
Das ist kein WordPress-Plugin-Problem. Es ist ein Memory-Safety-Bug in der PHP-Runtime selbst. Framework, CMS oder Composer-Abhängigkeiten zu aktualisieren bringt nichts. Wenn du ein Game-Backend mit irgendeinem PHP betreibst, ist dieses Runbook für dich.
Was php2shell tatsächlich anrichtet
Beide CVEs liegen darin, wie PHP mit Speicher umgeht, wenn die Request-Verarbeitung schiefgeht. Normalerweise allokiert PHP Speicher für einen Request, nutzt ihn und gibt ihn frei, wenn der Request endet. Die Bugs treten auf, wenn dieser Cleanup-Pfad gegen einen Zustand läuft, der von vornherein nie gültig war.
CVE-2026-109618: Out-of-Memory-Korruption
PHP erreicht sein Speicherlimit mitten im Request und bricht ab, bevor einige interne Strukturen fertig initialisiert sind. Die Cleanup-Routine behandelt diese halb aufgebauten Daten dann als legitim. Ein Angreifer, der zuverlässig OOM-Bedingungen erzwingen kann – große Request-Bodies, tief verschachtelte Eingaben, überdimensionierte Uploads – bekommt die Chance, zu kontrollieren, was dieser unfertige Speicher enthält.
Der praktische Auslöser für Game-Backends liegt auf der Hand: jeder Endpoint, der einen Body ohne Größenlimit akzeptiert. Match-Result-Einreichung, Screenshot-Upload, Mod-Metadaten, Telemetrie-Batches.
CVE-2026-109619: Veralteter globaler Zustand über Requests hinweg
Ein fataler Fehler unterbricht einen Request und lässt den PHP-Prozess Referenzen auf Speicher halten, der am Request-Ende freigegeben wird. Wenn derselbe Worker-Prozess den nächsten Request übernimmt, kann er diesen Dangling References folgen. Das ist Datenoffenlegung – die Session-Daten eines Spielers oder Datenbank-Zugangsdaten leaken in die Antwort eines anderen Spielers – und unter den richtigen Bedingungen Codeausführung.
Das ist der heimtückischere Bug für Game-Server, weil PHP-FPM-Worker von Natur aus langlebig sind. Ein einzelner vergifteter Worker kann hunderte nachfolgende Requests bedienen, bevor er recycelt wird. Wenn du pm.max_requests = 0 betreibst (die Standardeinstellung, also nie recyceln), kann ein korrupter Worker für die gesamte Lebensdauer des Containers bestehen bleiben.
Schritt 1: Inventar aller PHP-Runtimes, die du besitzt
Du kannst nicht patchen, was du nicht gefunden hast. Die meisten Studios entdecken mitten im Incident, dass drei ihrer Services PHP ausführen und einer davon auf 8.1 aus einem vor zwei Jahren gebauten Base-Image läuft. Führe das aus, bevor du irgendetwas anderes tust.
#!/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
Zwei Dinge macht dieses Skript bewusst: Es markiert EOL-Branches separat und geht nie davon aus, dass php -v auf dem Host die tatsächliche Runtime des Containers widerspiegelt. Wenn du Kubernetes nutzt, tausche docker compose exec gegen kubectl exec und iteriere über Deployments statt über Service-Namen.
Die behobenen Upstream-Versionen sind 8.5.12, 8.4.27, 8.3.36 und 8.2.35. Alles auf 8.1, 8.0, 7.x oder 5.x ist End-of-Life und wird nie einen Fix erhalten. Du führst ein Upgrade durch, kein Patching.
Schritt 2: Die Runtime patchen, nicht die Anwendung
Der häufigste Fehlermodus in der ersten Stunde eines solchen Incidents ist ein Team, das Anwendungscode aktualisiert, neu deployed, grünes CI sieht und annimmt, es sei fertig. Ist es nicht.
- PHP-FPM und mod_php: Baue das Base-Image neu oder aktualisiere das Paket. Debian/Ubuntu:
apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine:apk upgrade php83. Verifiziere mitphp -r 'echo PHP_VERSION;'innerhalb des laufenden Containers, nicht auf dem Build-Host. - FrankenPHP / php-cgi: Diese bündeln die Runtime, du brauchst also ein neues Release-Binary oder Base-Image, kein Paket-Upgrade.
- Managed Hosting: Wenn ein Provider PHP kontrolliert, öffne sofort ein Ticket und frag nach der Advisory-Referenz. Akzeptiere „wir sind informiert“ nicht als Lösung.
- Docker-Base-Images: Pinne per Digest, nicht per Tag.
php:8.3-fpmkann beim nächsten Pull auf einen anderen Build auflösen – großartig zum Patchen, schrecklich für Reproduzierbarkeit. Pinne den Digest, nachdem du die Version verifiziert hast.
Nach dem Patchen beende jeden vorhandenen Worker. Ein Rolling Restart, der alte Prozesse an einem gemeinsamen Socket am Leben lässt, hält verwundbare Worker im Traffic. docker compose restart oder kubectl rollout restart – bestätige, dass sich die Prozess-Startzeit geändert hat.
Schritt 3: Den Blast Radius verkleinern, während du wartest
Patchen dauert. Du hast ein Zeitfenster zwischen „wir wissen es“ und „jede Umgebung ist neu gebaut“. Nutze es.
Request-Bodies am Edge begrenzen
# 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;
}
Ein 2-MB-Limit stoppt keinen entschlossenen Angreifer – er kann OOM mit vielen kleinen Requests auslösen –, aber es eliminiert den trivialen Einzel-Request-Trigger und kostet dich nichts.
Worker aggressiv recyceln
; /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
Hier ist die Trade-off-Rechnung. Bei 200 Requests pro Sekunde über 24 Worker bearbeitet jeder Worker grob 8,3 req/s. Mit pm.max_requests = 500 wird jeder Worker etwa alle 60 Sekunden recycelt. Das begrenzt, wie lange ein durch CVE-2026-109619 korrupter Prozess Traffic bedienen kann, bedeutet aber auch 24 Prozess-Spawns pro Minute – messbarer Overhead bei Frameworks mit schwerem Kaltstart.
memory_limit von 128M auf 256M anzuheben reduziert, wie oft du OOM triffst (weniger CVE-2026-109618-Trigger), verdoppelt aber den Worst-Case-Speicher: 24 Children × 256M = 6 GB Headroom, den du jetzt auf dem Node brauchst. Wenn dein Node 4 GB hat, senke pm.max_children, statt so zu tun, als wäre der Speicher da.
Das ist kein Fix. Es kauft dir Stunden. Behandle es als Triage.
Schritt 4: Erkennen, ob du bereits getroffen wurdest
Disclosure ist nicht gleich erste Ausnutzung. Angriffstraffic wurde vor dem offiziellen Veröffentlichungsdatum beobachtet. Geh davon aus, dass du gescannt wurdest.
# 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
Zuerst die Baseline. Wenn dein PHP-FPM-Log bereits 40 OOM-Abbrüche pro Tag von einem speicherhungrigen Export-Endpoint hatte, ist diese Zahl Rauschen. Worauf du achtest, ist eine Änderung der Rate – einen Spike bei Signal-11-Exits, einen neuen Pfad in der 500-Liste oder eine einzelne IP, die Dutzende Fehler gegen einen Endpoint erzeugt.
Wenn du Hinweise auf erfolgreiche Ausnutzung findest, behandle es als vollständigen Breach: Rotiere Datenbank-Zugangsdaten, Session-Signing-Keys, Payment-Provider-Secrets und alle API-Keys, die vom PHP-Prozess aus erreichbar sind. Das Playbook dafür ist nicht PHP-spezifisch – dieselbe Disziplin, die einem Studio hilft, Game-Backends so zu architektieren, dass sie Kompromittierungen überstehen, gilt auch hier, und sie beginnt mit der Annahme, dass der Angreifer Codeausführung hatte, nicht nur Lesezugriff.
Wenn dein Backend bereits Requests verliert und du unsicher bist, ob es Last oder Angriff ist, gilt die Crash-Triage-Reihenfolge aus diesem Diagnoseprotokoll für Server-Freezes: Isoliere die fehlerhafte Ebene, bevor du die Konfiguration anfasst.
Warum reine Request-WAF-Regeln versagt haben
Der Cloudflare-Bericht enthält die übertragbarste Engineering-Lektion des gesamten Incidents. Ihre anfänglichen Schutzmaßnahmen waren request-basiert, und Angreifer reagierten mit mutierenden Payloads – sie testeten, welche Varianten matchten und welche durchrutschten, und nutzten das Match/No-Match-Signal als Orakel.
Request-Daten allein konnten Exploit-Traffic nicht von legitimem Traffic trennen, denn ein großer POST mit verschachtelten Arrays ist auch das, was ein normaler Game-Client sendet, wenn er eine Match-Zusammenfassung einreicht. Das Signal, das die Mehrdeutigkeit auflöste, war die Antwort: Lieferte der Origin einen 500 mit einem PHP-Fatal-Error-Trace? Diese Kombination – verdächtiger Request plus diagnostische Antwort – ist hochzuverlässig. Der Request allein ist es nicht.
Cloudflare lieferte das als „Full Transaction Detection“ aus, die Request und Response innerhalb derselben HTTP-Transaktion korreliert und die Response blockiert, bevor sie den Anforderer erreicht. Wenn du einen eigenen Edge oder Reverse Proxy betreibst, ist das Muster eine Kopie wert:
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
)
Die Lektion geht über dieses CVE hinaus: Jedes Erkennungssystem, das nur eingehenden Traffic sieht, wird von einem Angreifer umgangen, der das ausgehende Ergebnis sehen kann. Logge Responses. Korreliere sie. Das ist der Unterschied zwischen einer Regel, die hält, und einer Regel, die zum Rätsel wird.
Best Practices
- Nach Zeitplan patchen, nicht nach Disclosure. Die Lücke zwischen „CVE veröffentlicht“ und „Exploit in freier Wildbahn“ wird in Stunden gemessen, manchmal negativ. Baue PHP-Base-Images wöchentlich neu, auch wenn nichts angekündigt ist, und behalte einen Rollback-Pfad.
pm.max_requests = 0niemals in Produktion betreiben. Eine unbegrenzte Worker-Lebensdauer macht aus jedem Memory-Safety-Bug einen persistenten Fußpunkt. 500–1000 ist ein vernünftiger Standard für Game-APIs.- Jeden Request-Body am Edge begrenzen. 2 MB für JSON-APIs, 10 MB für Uploads, und alles ohne passenden Content-Type ablehnen. Das tötet ganze Klassen von Memory-Pressure-Exploits, bevor sie PHP erreichen.
- Response-Bodies für 5xx-Fehler loggen, gesampelt. Du kannst keine Full-Transaction-Detection aufbauen, wenn deine Origin-Responses für deinen Observability-Stack unsichtbar sind. Sample 10 % und behalte 7 Tage.
- Ein Inventar jeder Runtime führen, die du betreibst. Nicht nur PHP – Node, Python, Go, was auch immer deine Web-Ebene ausführt. Die Studios, die innerhalb einer Stunde auf php2shell reagiert haben, hatten einen Service-Katalog. Die, die einen Tag brauchten, haben Dockerfiles durchsucht.
Die größere Lektion: Deine Web-Ebene ist die Angriffsfläche
php2shell erinnert daran, dass Game-Backends mit der Zeit Altlasten ansammeln. Du lieferst im ersten Jahr ein PHP-Admin-Panel aus, weil es schnell zu bauen war. Im dritten Jahr ist es immer noch da, immer noch auf 8.1, hält immer noch eine Datenbank-Zugangsdaten mit Schreibzugriff auf die Spielertabelle – und es ist das, was geknackt wird, nicht deine gehärtete Game-Server-Binary.
Der strukturelle Fix ist, zu reduzieren, wie viel Runtime du besitzt. Wenn der einzige Grund, warum du eine PHP-Web-Ebene betreibst, Player-Accounts, Cloud Saves und Leaderboards sind, dann sind das genau die Fähigkeiten, die horizOn als verwaltete APIs bereitstellt – Endnutzer-Authentifizierung mit account-gebundenen Sessions, Cloud Save mit revisionsbewussten Reads und Writes, bei denen der Client Konflikte auflöst, Leaderboards und Remote-Konfiguration. Kein PHP-FPM-Pool, den du um 3 Uhr nachts patchen musst, weil es keinen PHP-FPM-Pool gibt.
Das ist kein Argument dafür, dass verwaltete Services immun gegen Sicherheitslücken sind. Es ist ein Argument dafür, dass jede Runtime, die du betreibst, eine Runtime ist, die du inventarisieren, patchen und überwachen musst – und dass die meisten Studios mehr davon haben, als sie denken.
Jetzt handeln
Wenn du eine einzige Aktion aus diesem Artikel mitnimmst, dann das Inventar-Skript aus Schritt 1. Führe es gegen jede Umgebung aus – Produktion, Staging, den vergessenen Demo-Server aus dem Publisher-Pitch. Du wirst mit ziemlicher Sicherheit eine Runtime finden, die du vergessen hast, und sie wird mit ziemlicher Sicherheit auf einem EOL-Branch sein.
Dann patchen, die Worker recyceln und die Edge-Regeln aktiv lassen. Die WAF-Regeln und Body-Limits sind Defense in Depth, kein Ersatz für den Vendor-Fix – das PHP-Advisory ist die Quelle der Wahrheit und das, was das Loch tatsächlich schließt.
Wenn das Inventar mehr PHP zutage fördert, als du künftig besitzen willst, deckt horizOn die Account-, Save- und Leaderboard-Ebene als verwaltete Services ab – das ist der schnellste Weg, die Fläche zu verkleinern, für deren Patching du verantwortlich bist. Wie auch immer: Erst Inventar, dann Patch, und hör auf anzunehmen, dass der langweilige Teil deines Stacks der sichere Teil ist.
Quelle: Cloudflare stellt Schutzmaßnahmen gegen die php2shell-Sicherheitslücke bereit