Retour au Blog

Protection des serveurs de jeu contre la vulnérabilité php2shell : le runbook de correctif en 6 heures

Publié le 23 octobre 2026
Protection des serveurs de jeu contre la vulnérabilité php2shell : le runbook de correctif en 6 heures Généré avec l'aide de l'IA

En bref

Découvrez le runbook en 6 heures pour corriger php2shell, protéger vos serveurs de jeu, inventorier vos runtimes PHP et réduire la surface d'attaque.

Le niveau PHP de votre jeu est probablement la partie de votre stack que personne ne regarde. La boutique web qui vend des cosmétiques. Le panneau d'administration utilisé par votre community manager. L'endpoint de classement hérité que vous avez écrit en 2022 et plus jamais touché. C'est cette surface que php2shell attaque.

Le 21 octobre 2026, Calif.io, Wiz, OpenAI et Quarkslab ont révélé deux vulnérabilités du runtime PHP référencées CVE-2026-109618 et CVE-2026-109619, collectivement nommées php2shell. Le CVSS v4.0 leur attribue un score de 9.2. Elles affectent toutes les installations PHP depuis la 5.4, permettent l'exécution de code à distance sans authentification, l'exposition de données et le déni de service, et Cloudflare a observé du trafic d'attaque en direct mutant des payloads contre de vrais clients avant même la divulgation publique.

Ce n'est pas un problème de plugin WordPress. C'est un bug de sécurité mémoire dans le runtime PHP lui-même. Mettre à jour votre framework, votre CMS ou vos dépendances Composer ne change rien. Si vous opérez un backend de jeu contenant du PHP, ce runbook est fait pour vous.

Ce que php2shell casse réellement

Les deux CVE se situent dans la façon dont PHP gère la mémoire quand le traitement d'une requête déraille. Normalement, PHP alloue de la mémoire pour une requête, l'utilise, puis la libère à la fin de la requête. Les bugs apparaissent lorsque ce chemin de nettoyage s'exécute sur un état qui n'a jamais été valide.

CVE-2026-109618 : corruption mémoire sur dépassement de mémoire

PHP atteint son plafond mémoire en pleine requête et abandonne avant que certaines structures internes finissent de s'initialiser. La routine de nettoyage traite alors ces données à moitié construites comme légitimes. Un attaquant qui peut forcer de manière fiable des conditions d'OOM — corps de requête volumineux, entrées profondément imbriquées, uploads surdimensionnés — a une chance de contrôler ce que contient cette mémoire inachevée.

Le déclencheur pratique pour les backends de jeu est évident : tout endpoint qui accepte un corps sans limite de taille. Soumission de résultats de match, upload de captures d'écran, métadonnées de mods, lots de télémétrie.

CVE-2026-109619 : état global obsolète entre les requêtes

Une erreur fatale interrompt une requête, laissant le processus PHP conserver des références vers de la mémoire libérée à la fin de la requête. Lorsque le même processus worker prend la requête suivante, il peut suivre ces références pendantes. C'est une exposition de données — les données de session d'un joueur ou des identifiants de base de données qui fuient dans la réponse d'un autre joueur — et, dans les bonnes conditions, une exécution de code.

C'est le bug le plus insidieux pour les serveurs de jeu car les workers PHP-FPM sont conçus pour vivre longtemps. Un seul worker empoisonné peut servir des centaines de requêtes avant d'être recyclé. Si vous utilisez pm.max_requests = 0 (la valeur par défaut, ce qui signifie jamais recyclé), un worker corrompu peut persister pendant toute la durée de vie du conteneur.

Étape 1 : Inventorier chaque runtime PHP que vous possédez

Vous ne pouvez pas corriger ce que vous n'avez pas trouvé. La plupart des studios découvrent, en plein incident, que trois de leurs services tournent en PHP et que l'un d'eux est en 8.1 sur une image de base construite il y a deux ans. Exécutez ceci avant toute autre action.

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

Ce script fait deux choses volontairement : il signale les branches EOL séparément, et il ne suppose jamais que php -v sur l'hôte reflète le runtime réel du conteneur. Si vous êtes sur Kubernetes, remplacez docker compose exec par kubectl exec et itérez sur les déploiements plutôt que sur les noms de services.

Les versions amont corrigées sont 8.5.12, 8.4.27, 8.3.36 et 8.2.35. Tout ce qui tourne en 8.1, 8.0, 7.x ou 5.x est en fin de vie (EOL) et ne recevra jamais de correctif. Vous faites une mise à niveau, pas un simple patch.

Étape 2 : Corriger le runtime, pas l'application

Le mode d'échec le plus courant dans la première heure d'un tel incident est une équipe qui met à jour le code applicatif, redéploie, voit le CI passer au vert et pense avoir terminé. Ce n'est pas le cas.

  • PHP-FPM et mod_php : reconstruisez l'image de base ou mettez à jour le paquet. Debian/Ubuntu : apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine : apk upgrade php83. Vérifiez avec php -r 'echo PHP_VERSION;' à l'intérieur du conteneur en cours d'exécution, pas sur l'hôte de build.
  • FrankenPHP / php-cgi : ils incluent le runtime, vous avez donc besoin d'un binaire de nouvelle version ou d'une nouvelle image de base, pas d'une mise à jour de paquet.
  • Hébergement managé : si un fournisseur contrôle PHP, ouvrez un ticket immédiatement et demandez la référence de l'avis de sécurité. N'acceptez pas « nous sommes au courant » comme résolution.
  • Images de base Docker : épinglez par digest, pas par tag. php:8.3-fpm peut résoudre vers un build différent au prochain pull, ce qui est idéal pour le patching et terrible pour la reproductibilité. Épinglez le digest après avoir vérifié la version.

Après le patch, tuez chaque worker existant. Un redémarrage progressif qui laisse les anciens processus vivants sur un socket partagé continue de servir du trafic avec des workers vulnérables. docker compose restart ou kubectl rollout restart — confirmez que l'heure de démarrage du processus a changé.

Étape 3 : Réduire le rayon d'explosion en attendant

Le patch prend du temps. Vous avez une fenêtre entre « nous savons » et « tous les environnements sont reconstruits ». Utilisez-la.

Limiter les corps de requête en périphérie

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

Une limite de 2 Mo n'arrête pas un attaquant déterminé — il peut déclencher un OOM avec de nombreuses petites requêtes — mais elle élimine le déclencheur trivial en une seule requête et ne vous coûte rien.

Recycler les workers agressivement

; /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

Voici le calcul du compromis. À 200 requêtes par seconde sur 24 workers, chaque worker gère environ 8,3 req/s. Avec pm.max_requests = 500, chaque worker est recyclé toutes les ~60 secondes. Cela plafonne la durée pendant laquelle un processus corrompu par CVE-2026-109619 peut servir du trafic, mais cela signifie aussi 24 spawns de processus par minute — un overhead mesurable sur les frameworks lourds au démarrage.

Augmenter memory_limit de 128M à 256M réduit la fréquence des OOM (moins de déclenchements de CVE-2026-109618) mais double la mémoire maximale : 24 enfants × 256M = 6 Go de marge désormais nécessaires sur le nœud. Si votre nœud a 4 Go, baissez pm.max_children au lieu de faire comme si la mémoire existait.

Rien de tout cela n'est un correctif. Cela vous fait gagner des heures. Traitez-le comme un triage.

Étape 4 : Détecter si vous avez déjà été touché

La divulgation ne signifie pas que l'exploitation est récente. Du trafic d'attaque a été observé avant la date de publication officielle. Supposez que vous avez été sondé.

# 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

Établissez d'abord une baseline. Si votre log PHP-FPM avait déjà 40 abandons OOM par jour à cause d'un endpoint d'export gourmand en mémoire, ce nombre est du bruit. Ce que vous cherchez, c'est un changement de rythme — un pic de sorties signal 11, un nouveau chemin apparaissant dans la liste des 500, ou une seule IP générant des dizaines d'échecs contre un même endpoint.

Si vous trouvez des preuves d'exploitation réussie, traitez cela comme une compromission complète : changez les identifiants de base de données, les clés de signature de session, les secrets du fournisseur de paiement et toutes les clés API accessibles depuis le processus PHP. Le playbook pour cela n'est pas spécifique à PHP — la même discipline qui permet à un studio d'architecturer des backends de jeu pour survivre aux compromissions s'applique ici, et cela commence par supposer que l'attaquant avait une exécution de code, pas seulement un accès en lecture.

Si votre backend perd déjà des requêtes et que vous ne savez pas s'il s'agit d'une surcharge ou d'une attaque, l'ordre de triage des crashs de ce protocole de diagnostic des gels de serveur s'applique : isolez la couche défaillante avant de toucher à la configuration.

Pourquoi les règles WAF basées uniquement sur la requête ont échoué

Le rapport de Cloudflare contient la leçon d'ingénierie la plus transférable de tout cet incident. Leurs protections initiales étaient basées sur les requêtes, et les attaquants ont répondu en mutant les payloads — sondant quelles variantes correspondaient et lesquelles passaient, en utilisant le signal correspondance/non-correspondance comme oracle.

Les données de requête seules ne pouvaient pas séparer le trafic d'exploitation du trafic légitime, car un gros POST avec des tableaux imbriqués est aussi ce qu'un client de jeu normal envoie lorsqu'il soumet un résumé de match. Le signal qui a levé l'ambiguïté était la réponse : l'origine renvoyait-elle un 500 avec une trace d'erreur fatale PHP ? Cette combinaison — requête suspecte plus réponse de diagnostic — est très fiable. La requête seule ne l'est pas.

Cloudflare a présenté cela comme « détection de transaction complète », corrélant requête et réponse dans la même transaction HTTP et bloquant la réponse avant qu'elle n'atteigne le demandeur. Si vous gérez votre propre edge ou reverse proxy, le schéma vaut la peine d'être copié :

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 leçon va au-delà de cette CVE : tout système de détection qui ne voit que le trafic entrant sera contourné par un attaquant qui peut voir le résultat sortant. Journalisez les réponses. Corrélez-les. C'est la différence entre une règle qui tient et une règle qui devient une énigme.

Meilleures pratiques

  1. Patchez selon un calendrier, pas selon les divulgations. L'écart entre « CVE publiée » et « exploit dans la nature » se mesure désormais en heures, parfois négatif. Reconstruisez les images de base PHP chaque semaine même sans annonce, et gardez un chemin de rollback.
  2. Ne lancez jamais pm.max_requests = 0 en production. Une durée de vie illimitée des workers transforme chaque bug de sécurité mémoire en point d'appui persistant. 500–1000 est une valeur par défaut raisonnable pour les API de jeu.
  3. Limitez chaque corps de requête en périphérie. 2 Mo pour les API JSON, 10 Mo pour les uploads, et rejetez tout ce qui n'a pas un type de contenu correspondant. Cela tue des classes entières d'exploits de pression mémoire avant qu'ils n'atteignent PHP.
  4. Journalisez les corps de réponse pour les erreurs 5xx, avec échantillonnage. Vous ne pouvez pas construire de détection de transaction complète si les réponses de votre origine sont invisibles pour votre stack d'observabilité. Échantillonnez 10 % et conservez 7 jours.
  5. Tenez un inventaire de chaque runtime que vous opérez. Pas seulement PHP — Node, Python, Go, ou tout ce qui fait tourner votre couche web. Les studios qui ont répondu à php2shell en moins d'une heure avaient un catalogue de services. Ceux qui ont mis une journée fouillaient leurs Dockerfiles.

La leçon plus large : votre couche web est la surface d'attaque

php2shell rappelle que les backends de jeu accumulent des reliquats techniques. Vous livrez un panneau d'administration PHP en première année parce que c'était rapide à construire. En troisième année, il est toujours là, toujours en 8.1, toujours avec un identifiant de base de données en écriture sur la table des joueurs — et c'est lui qui se fait compromettre, pas votre binaire de serveur de jeu durci.

La solution structurelle consiste à réduire le nombre de runtimes dont vous êtes propriétaire. Si la seule raison pour laquelle vous opérez une couche web PHP est de gérer les comptes joueurs, les sauvegardes cloud et les classements, ce sont exactement les capacités que horizOn expose en tant qu'API managées — authentification des utilisateurs finaux avec sessions liées au compte, Cloud Save avec lectures et écritures conscientes des révisions où le client résout les conflits, classements et configuration à distance. Pas de pool PHP-FPM à patcher à 3 h du matin, car il n'y a pas de pool PHP-FPM.

Ce n'est pas un argument pour dire que les services managés sont immunisés contre les vulnérabilités. C'est un argument pour dire que chaque runtime que vous opérez est un runtime que vous devez inventorier, patcher et surveiller — et que la plupart des studios en ont plus qu'ils ne le croient.

Faites ceci maintenant

Si vous ne retenez qu'une action de cet article, que ce soit le script d'inventaire de l'étape 1. Exécutez-le sur tous les environnements — production, staging, le serveur de démo oublié depuis ce pitch éditeur. Vous trouverez presque certainement un runtime que vous aviez oublié, et il sera presque certainement sur une branche EOL.

Ensuite, patchez, recyclez les workers et laissez les règles périphériques en place. Les règles WAF et les limites de corps sont une défense en profondeur, pas un substitut au correctif du fournisseur — l'avis de sécurité PHP est la source de vérité et c'est lui qui ferme réellement la brèche.

Si l'inventaire révèle plus de PHP que vous ne voulez en conserver à l'avenir, horizOn couvre la couche comptes, sauvegardes et classements en tant que services managés, ce qui est le moyen le plus rapide de réduire la surface dont vous êtes responsable du patching. Dans tous les cas : inventaire d'abord, patch ensuite, et arrêtez de supposer que la partie ennuyeuse de votre stack est la partie sûre.


Source : Cloudflare déploie des protections contre la vulnérabilité php2shell