Terug naar Blog

php2shell-kwetsbaarheid: het 6-uurs patch-draaiboek voor game-serverbescherming

Gepubliceerd op 23 oktober 2026
php2shell-kwetsbaarheid: het 6-uurs patch-draaiboek voor game-serverbescherming Gegenereerd met behulp van AI

Kort samengevat

Patch je game-backend binnen 6 uur: inventariseer PHP-runtimes, los CVE-2026-109618 en CVE-2026-109619 op en verklein het aanvalsoppervlak.

De PHP-laag van je game is waarschijnlijk het onderdeel van je stack waar niemand naar kijkt. De webshop die cosmetics verkoopt. Het adminpaneel dat je communitymanager gebruikt. De legacy leaderboard-endpoint die je in 2022 schreef en nooit meer aanraakte. Dat is het aanvalsoppervlak van php2shell.

Op 21 oktober 2026 maakten Calif.io, Wiz, OpenAI en Quarkslab twee PHP-runtimekwetsbaarheden bekend, geregistreerd als CVE-2026-109618 en CVE-2026-109619, gezamenlijk php2shell genoemd. CVSS v4.0 beoordeelt ze met 9.2. Ze treffen elke PHP-installatie vanaf 5.4, maken niet-geauthenticeerde remote code execution, data-exposure en denial of service mogelijk, en Cloudflare zag al live aanvalsverkeer payloads muteren tegen echte klanten voordat de publieke disclosure er was.

Dit is geen WordPress-pluginprobleem. Het is een geheugenveiligheidsbug in de PHP-runtime zelf. Je framework, je CMS of je Composer-afhankelijkheden bijwerken doet niets. Als je een game-backend draait met ook maar enige PHP erin, is dit draaiboek voor jou.

Wat php2shell daadwerkelijk breekt

Beide CVE's zitten in de manier waarop PHP met geheugen omgaat wanneer requestverwerking misgaat. Normaal reserveert PHP geheugen voor een request, gebruikt het en geeft het weer vrij wanneer het request eindigt. De bugs verschijnen wanneer dat opruimpad draait tegen state die nooit geldig was.

CVE-2026-109618: out-of-memory-corruptie

PHP raakt halverwege een request zijn geheugenlimiet en breekt af voordat sommige interne structuren klaar zijn met initialiseren. De opruimroutine behandelt die half opgebouwde data vervolgens als legitiem. Een aanvaller die betrouwbaar OOM-omstandigheden kan forceren — grote request bodies, diep geneste input, overmatig grote uploads — krijgt een kans om te controleren wat dat onafgewerkte geheugen bevat.

De praktische trigger voor game-backends is duidelijk: elk endpoint dat een body accepteert zonder groottebeperking. Matchresultaat-inzending, screenshot-upload, mod-metadata, telemetrie-batches.

CVE-2026-109619: verouderde globale state tussen requests

Een fatale fout onderbreekt een request, waardoor het PHP-proces verwijzingen naar geheugen vasthoudt dat aan het einde van het request wordt vrijgegeven. Wanneer hetzelfde worker-proces het volgende request oppakt, kan het die dangling references volgen. Dat is data-exposure — de sessiegegevens of databasecredentials van de ene speler die in de response van een andere speler lekken — en onder de juiste omstandigheden code-execution.

Dit is de verraderlijkste bug voor game servers omdat PHP-FPM-workers per definitie lang leven. Een enkel vergiftigd worker kan honderden opeenvolgende requests bedienen voordat het wordt gerecycled. Als je pm.max_requests = 0 draait (de standaard, wat betekent nooit recyclen), kan een gecorrumpeerd worker blijven bestaan zolang de container leeft.

Stap 1: Inventariseer elke PHP-runtime die je bezit

Je kunt niet patchen wat je niet hebt gevonden. De meeste studio's ontdekken halverwege een incident dat drie van hun services PHP draaien en dat één daarvan op 8.1 staat van een basisimage die twee jaar geleden is gebouwd. Draai dit voordat je iets anders doet.

#!/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

Dit script doet twee dingen met opzet: het markeert EOL-branches apart, en het gaat er nooit vanuit dat php -v op de host de werkelijke runtime van de container weerspiegelt. Als je op Kubernetes draait, vervang je docker compose exec door kubectl exec en itereer je over deployments in plaats van servicenamen.

De gefixte upstream-versies zijn 8.5.12, 8.4.27, 8.3.36 en 8.2.35. Alles op 8.1, 8.0, 7.x of 5.x is end-of-life en krijgt nooit een fix. Je bent aan het upgraden, niet aan het patchen.

Stap 2: Patch de runtime, niet de applicatie

De meest voorkomende faalmodus in het eerste uur van zo'n incident is een team dat applicatiecode bijwerkt, opnieuw deployt, groene CI ziet en denkt dat ze klaar zijn. Dat zijn ze niet.

  • PHP-FPM en mod_php: herbouw de basisimage of verhoog het pakket. Debian/Ubuntu: apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine: apk upgrade php83. Verifieer met php -r 'echo PHP_VERSION;' binnen de draaiende container, niet op de build-host.
  • FrankenPHP / php-cgi: deze bundelen de runtime, dus je hebt een nieuwe releasebinary of basisimage nodig, geen pakketverhoging.
  • Managed hosting: als een provider PHP beheert, open dan onmiddellijk een ticket en vraag naar de advisory-referentie. Accepteer "we zijn op de hoogte" niet als oplossing.
  • Docker-basisimages: pin op digest, niet op tag. php:8.3-fpm kan bij de volgende pull naar een andere build verwijzen; geweldig voor patchen, vreselijk voor reproduceerbaarheid. Pin de digest nadat je de versie hebt geverifieerd.

Dood na het patchen elk bestaand worker. Een rolling restart die oude processen in leven laat op een gedeelde socket, houdt kwetsbare workers verkeer bedienen. docker compose restart of kubectl rollout restart — bevestig dat de starttijd van het proces is veranderd.

Stap 3: Verklein de impactradius terwijl je wacht

Patchen kost tijd. Je hebt een venster tussen "we weten het" en "elke omgeving is herbouwd". Gebruik het.

Begrens request bodies aan de rand

# 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;
}

Een limiet van 2 MB stopt een vastberaden aanvaller niet — ze kunnen OOM triggeren met veel kleine requests — maar het elimineert de triviale single-request-trigger en het kost je niets.

Recycle workers agressief

; /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 is de ruil-rekenkunde. Bij 200 requests per seconde over 24 workers verwerkt elk worker ruwweg 8,3 req/s. Met pm.max_requests = 500 wordt elk worker elke ~60 seconden gerecycled. Dat beperkt hoe lang een door CVE-2026-109619 gecorrumpeerd proces verkeer kan bedienen, maar het betekent ook 24 processpawns per minuut — meetbare overhead op frameworks met zware cold starts.

Het verhogen van memory_limit van 128M naar 256M vermindert hoe vaak je OOM raakt (minder CVE-2026-109618-triggers), maar verdubbelt het worst-case geheugen: 24 children × 256M = 6 GB headroom die je nu op de node nodig hebt. Als je node 4 GB heeft, verlaag dan pm.max_children in plaats van te doen alsof het geheugen er is.

Niets hiervan is een fix. Het koopt je uren. Behandel het als triage.

Stap 4: Detecteer of je al geraakt bent

Disclosure staat niet gelijk aan eerste exploitatie. Er is aanvalsverkeer waargenomen vóór de officiële releasedatum. Ga ervan uit dat je bent geprobeerd.

# 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

Eerst een baseline. Als je PHP-FPM-log al 40 OOM-afbrekingen per dag had van een geheugenhongerig export-endpoint, dan is dat getal ruis. Waar je naar zoekt is een verandering in het tempo — een piek in signal 11-exits, een nieuw pad dat in de 500-lijst verschijnt, of één IP dat tientallen fouten tegen één endpoint genereert.

Als je bewijs vindt van succesvolle exploitatie, behandel het dan als een volledige breach: rotatie van databasecredentials, sessie-ondertekeningssleutels, geheimen van betalingsproviders en alle API-sleutels die vanuit het PHP-proces bereikbaar zijn. Het draaiboek daarvoor is niet PHP-specifiek — dezelfde discipline waarmee een studio game-backends zo bouwt dat ze compromissen overleven geldt hier, en het begint met de aanname dat de aanvaller code-execution had, niet alleen leestoegang.

Als je backend al requests laat vallen en je weet niet zeker of het load of een aanval is, geldt de crash-triagevolgorde in dit diagnoseprotocol voor serverfreezes: isoleer de falende laag voordat je configuratie aanraakt.

Waarom request-only-WAF-regels faalden

Cloudflare's write-up bevat de meest overdraagbare engineeringles van het hele incident. Hun eerste beschermingen waren request-gebaseerd, en aanvallers reageerden door payloads te muteren — ze testten welke varianten overeenkwamen en welke erdoor glipten, met het match/no-match-signaal als oracle.

Requestdata alleen kon exploitverkeer niet scheiden van legitiem verkeer, want een grote POST met geneste arrays is ook wat een normale gameclient verstuurt wanneer hij een matchoverzicht indient. Het signaal dat de dubbelzinnigheid oploste was de response: gaf de origin een 500 terug met een PHP-fatal-error-trace? Die combinatie — verdacht request plus diagnostische response — is met hoge betrouwbaarheid. Het request alleen niet.

Cloudflare leverde dit als "full transaction detection", waarbij request en response binnen dezelfde HTTP-transactie worden gecorreleerd en de response wordt geblokkeerd voordat deze de aanvrager bereikt. Als je je eigen edge of reverse proxy draait, is dit patroon het kopiëren waard:

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
    )

De les generaliseert voorbij deze CVE: elk detectiesysteem dat alleen inkomend verkeer ziet, wordt omzeild door een aanvaller die het uitgaande resultaat kan zien. Log responses. Correlateer ze. Het is het verschil tussen een regel die standhoudt en een regel die een puzzel wordt.

Best practices

  1. Patch op schema, niet op disclosure. De kloof tussen "CVE gepubliceerd" en "exploit in het wild" wordt nu in uren gemeten, soms negatief. Herbouw PHP-basisimages wekelijks, zelfs als er niets is aangekondigd, en houd een terugdraaipad.
  2. Draai nooit pm.max_requests = 0 in productie. Een onbegrensde worker-levensduur verandert elke geheugenveiligheidsbug in een blijvend steunpunt. 500–1000 is een verstandige standaard voor game-API's.
  3. Begrens elke request body aan de rand. 2 MB voor JSON-API's, 10 MB voor uploads, en verwerp alles zonder overeenkomend content type. Dit doodt hele klassen van geheugendruk-exploits voordat ze PHP bereiken.
  4. Log response bodies voor 5xx-fouten, gesampled. Je kunt geen full-transaction-detectie bouwen als je origin-responses onzichtbaar zijn voor je observability-stack. Sample 10% en bewaar 7 dagen.
  5. Houd een inventaris bij van elke runtime die je draait. Niet alleen PHP — Node, Python, Go, wat je web tier ook draait. De studio's die binnen een uur op php2shell reageerden hadden een servicecatalogus. De studio's die er een dag over deden waren Dockerfiles aan het greppen.

De grotere les: je web tier is het aanvalsoppervlak

php2shell is een herinnering dat game-backends rommel ophopen. Je levert in jaar één een PHP-adminpaneel op omdat het snel te bouwen was. In jaar drie staat het er nog steeds, nog steeds op 8.1, nog steeds met een databasecredential met schrijftoegang tot de spelertabel — en dat is wat wordt gekraakt, niet je geharde game-serverbinary.

De structurele fix is om te verminderen hoeveel runtime je bezit. Als de enige reden dat je een PHP-web tier draait het afhandelen van spelersaccounts, cloud saves en leaderboards is, dan zijn dat precies de mogelijkheden die horizOn als managed API's aanbiedt — end-user-authenticatie met accountgebonden sessies, Cloud Save met revisiebewuste reads en writes waarbij de client conflicten oplost, leaderboards en remote configuratie. Geen PHP-FPM-pool om om 3 uur 's nachts te patchen, want er is geen PHP-FPM-pool.

Dat is geen argument dat managed services immuun zijn voor kwetsbaarheden. Het is een argument dat elke runtime die je draait een runtime is die je moet inventariseren, patchen en monitoren — en dat de meeste studio's er meer hebben dan ze denken.

Doe dit nu

Als je één actie uit dit artikel meeneemt, laat het dan het inventarisatiescript uit Stap 1 zijn. Draai het tegen elke omgeving — productie, staging, de vergeten demoserver van die uitgeverspitch. Je zult vrijwel zeker een runtime vinden die je was vergeten, en die staat vrijwel zeker op een EOL-branch.

Patch daarna, recycle de workers en laat de edge-regels staan. De WAF-regels en de body-limieten zijn defense in depth, geen vervanging voor de vendorfix — de PHP-advisory is de bron van waarheid en dat is wat het gat daadwerkelijk sluit.

Als de inventaris meer PHP oplevert dan je in de toekomst wilt beheren, dekt horizOn de account-, save- en leaderboard-laag als managed services, wat de snelste manier is om het oppervlak te verkleinen waarvoor jij verantwoordelijk bent om te patchen. Hoe dan ook: eerst inventariseren, dan patchen, en stop met aannemen dat het saaie deel van je stack het veilige deel is.


Bron: Cloudflare implementeert beschermingen tegen php2shell-kwetsbaarheid