Le 21 octobre 2026, l'équipe de PHP a publié une version de sécurité un peu particulière. Deux failles critiques, notées 9,2 sur 10 en CVSS 4.0, permettent d'exécuter du code à distance sur un serveur, sans authentification. Surnommées PHP2Shell, elles ont fuité avant la publication coordonnée, et Cloudflare signalait déjà des tentatives d'attaque avant même la sortie des correctifs.
Point important : les failles sont dans PHP lui-même. Pas dans WordPress, pas dans un plugin, pas dans votre code. N'importe quel site PHP non mis à jour est donc potentiellement concerné.
Comme d'habitude ici, on va regarder ce qui se passe sous le capot : comment fonctionnent ces deux failles, ce que change le correctif dans php-src, et quoi faire si vous ne pouvez pas mettre à jour tout de suite. Mais d'abord, un peu de contexte.
Pourquoi c'est sérieux : PHP, c'est encore une bonne partie du web
On a parfois l'impression que PHP est une technologie du passé. Les chiffres disent l'inverse :
- 69,3 % des sites web dont on connaît le langage serveur utilisent PHP (W3Techs, 22 octobre 2026) ;
- WordPress, écrit en PHP, fait tourner à lui seul 39,6 % de tous les sites web (W3Techs, 21 octobre 2026). Drupal, Joomla, Magento, PrestaShop ou les panneaux d'hébergement comme cPanel et Plesk reposent eux aussi sur PHP ;
- 34,7 % des sites PHP tournent encore sur PHP 7 ou une version plus ancienne (W3Techs). Or seules les branches 8.2 à 8.5 bénéficient encore de la maintenance habituelle du projet PHP (php.net). Pour PHP2Shell, l'équipe PHP a exceptionnellement publié des correctifs source pour PHP 7.4, 8.0 et 8.1 (annonce PHP), et des distributions ou fournisseurs LTS proposent des paquets corrigés, y compris pour des versions plus anciennes (Ubuntu Pro, par exemple). Encore faut-il que ces sites les appliquent.
L'impact potentiel est maximal. Une exécution de code à distance, sans authentification, permet à un attaquant de faire tourner ce qu'il veut sur le serveur, avec les droits du processus web : lire la base de données et les fichiers de configuration (mots de passe, clés d'API), installer une porte dérobée, injecter du code malveillant dans les pages, ou se servir de la machine pour attaquer ailleurs. Sur un hébergement mutualisé, un seul site vulnérable peut exposer ses voisins.
Et la menace n'est pas théorique. Cloudflare indique que la faille a fuité avant la date de publication prévue, qu'il a observé du trafic d'attaque visant ses clients et déployé des règles de pare-feu environ neuf heures avant la divulgation publique (Cloudflare). Selon la presse, le groupe DPG Media a par précaution mis certains de ses sites hors ligne (ITdaily).
L'essentiel en une minute
- CVE-2026-109618 (GHSA-w8hw-w9fx-j79w) : corruption de l'état de la requête lors d'un épuisement mémoire.
- CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp) : un état global qui survit à la fin d'une requête, d'où une use-after-free.
- Versions corrigées : 8.2.35, 8.3.36, 8.4.27 et 8.5.12. Les branches 7.4, 8.0 et 8.1 ne sont plus maintenues, mais l'équipe PHP a exceptionnellement publié des correctifs source pour elles.
- Un seul commit corrige les deux : 3584a98 « Avoid state corruption via bailout ».
CVE-2026-109618 : quand un manque de mémoire corrompt la requête
Le bailout, un longjmp() bien pratique... et dangereux
Quand un script dépasse memory_limit, PHP ne peut pas simplement renvoyer une erreur à la fonction qui a demandé la mémoire : il abandonne tout le traitement en cours. Historiquement, ce « bailout » repose sur longjmp(), qui fait sauter l'exécution directement vers un point de reprise, sans dérouler proprement ce qui était en cours.
Le problème, c'est le moment où ça arrive. Dans certains contextes, de la mémoire de la requête est déjà visible avant d'être entièrement initialisée. Si une allocation échoue à cet instant précis, le saut laisse cette mémoire dans un état invalide. La phase d'arrêt de la requête (le shutdown) va ensuite la lire, ou prendre des décisions selon son contenu. Et l'allocation qui échoue n'a même pas besoin d'avoir un rapport avec la zone corrompue.
Provoquer l'épuisement mémoire avec une requête multipart
Reste à faire manquer de mémoire à PHP, à distance. L'avis décrit plusieurs vecteurs, tous dans le parseur des requêtes multipart/form-data (le fichier main/rfc1867.c), celui qui traite les formulaires avec envoi de fichiers :
- une requête multipart sans longueur de contenu exploitable peut passer la vérification initiale de
post_max_sizeet continuer à fournir des données ; - une partie multipart peut contenir un nombre illimité d'en-têtes, qui occupent bien plus de place en mémoire dans PHP que sur le réseau ;
- les champs de fichier vides ne comptent pas dans
max_file_uploads, mais créent quand même leurs tableaux de métadonnées, et des noms de champs imbriqués multiplient ce travail.
Résultat : une requête qui respecte post_max_size peut quand même atteindre memory_limit.
Tous les serveurs ne sont pas égaux
Avec Apache et mod_php, le shutdown est appelé même quand le démarrage de la requête a échoué. Le corps de la requête est lu pendant ce démarrage, avant tout code applicatif : l'exploitation complète a été validée en HTTP, avec les limites par défaut. Avec PHP-FPM, un échec au démarrage tue le processus enfant, ce qui limite l'attaque, mais un épuisement mémoire plus tardif reste possible : le code est jugé vulnérable, sans exploitation complète démontrée. FrankenPHP et LiteSpeed (LSAPI) sont considérés comme probablement vulnérables.
CVE-2026-109619 : l'état global qui survit à la requête
La seconde faille joue sur une autre faiblesse. PHP garde dans des variables globales des informations qui appartiennent à la requête en cours : erreurs, session, objets, données d'extensions. Elles doivent être remises à zéro avant que la mémoire de la requête soit libérée.
Si une erreur fatale interrompt une opération avant cette remise à zéro (épuisement mémoire, dépassement du temps d'exécution, erreur de compilation...), le worker, qui lui reste en vie pour la requête suivante, garde des pointeurs vers de la mémoire libérée. Il suffit alors qu'une opération de la requête suivante lise cet état périmé avant son initialisation normale : c'est une use-after-free.
Une erreur fatale n'est d'ailleurs pas indispensable à tous les scénarios : un simple avertissement émis pendant le nettoyage de fin de requête, après la remise à zéro, peut réintroduire un état d'erreur qui pointe vers la mémoire de la requête.
L'avis la décrit comme une chaîne à deux maillons : un producteur, qui laisse un état périmé, et un consommateur, qui le lit trop tôt. Deux consommateurs ont été confirmés :
- les diagnostics de configuration : un avertissement émis en appliquant un réglage INI, avant l'initialisation du moteur, touche l'état d'erreur. Sous mod_php, les directives
php_valueou celles d'un.htaccesssont appliquées à ce moment-là. Masquer les erreurs ne protège pas : l'accès a lieu avant la décision d'afficher ou non le message ; - la progression des uploads en session (
session.upload_progress) : PHP met à jour la session pendant la lecture du corps de la requête, avant son initialisation normale, et peut réutiliser un état laissé par une requête interrompue.
Sous PHP-FPM, le scénario validé n'est pas tout à fait celui de mod_php. Comme un échec au démarrage tue le worker, l'état périmé doit être publié pendant l'exécution de l'application, après un démarrage réussi, puis une erreur fatale doit survenir (dans l'avis, un épuisement mémoire pendant le traitement des données reçues). La requête suivante, si elle tombe sur le même worker, le relit ensuite via la progression d'upload. L'exploitation dépend donc d'une opération applicative appropriée.
Sur PHP 8.5, l'avis décrit aussi un producteur particulièrement efficace : avec fatal_error_backtraces activé, la trace d'une erreur fatale n'est jamais remise à zéro à l'arrêt. Un diagnostic INI de la requête suivante relit cet état avant l'initialisation : c'est la base de l'exécution de code confirmée contre mod_php.
Ce que change le correctif
Le commit 3584a98 corrige les deux failles d'un coup. Son message résume l'idée en deux lignes : remplacer le bailout sur épuisement mémoire par un UnwindExit, et remettre correctement à zéro les globales qui peuvent être lues au démarrage.
Fini le longjmp() sur épuisement mémoire
Le gestionnaire de mémoire (Zend/zend_alloc.c) ne saute plus n'importe où quand memory_limit est atteint. Il fonctionne maintenant en deux temps :
- une limite douce : l'erreur est enregistrée et traitée au prochain point sûr de la machine virtuelle, en déroulant proprement la pile ;
- une limite dure, plus haute : si la consommation continue de grimper, le processus s'arrête immédiatement avec
_exit(), sans jamais exécuter de shutdown sur un état douteux.
L'écart entre les deux est réglable avec une nouvelle directive, memory_hard_limit_percent. Par défaut, la limite dure vaut 200 % de memory_limit en dessous de 1 Go, et memory_limit plus 1 Go au-delà. Le suivi de l'erreur différée tient dans une petite structure ajoutée à Zend/zend_alloc.h :
typedef enum _zend_oom_state {
ZEND_OOM_STATE_NONE = 0,
ZEND_OOM_STATE_PENDING,
ZEND_OOM_STATE_HANDLING,
ZEND_OOM_STATE_HANDLED
} zend_oom_state;
Le commit introduit aussi des allocations « faillibles » (femalloc(), ferealloc(), safe_ferealloc()...), qui renvoient NULL au lieu de déclencher l'erreur fatale. Le code qui sait gérer un échec, comme le parseur multipart, peut ainsi s'arrêter proprement.
Un parseur multipart plus strict
Dans main/rfc1867.c, le parseur ne garde plus que les en-têtes dont il a besoin et ignore les doublons. Il vérifie aussi post_max_size sur les octets réellement lus, et plus seulement sur la longueur annoncée par le client.
Une session qui ne fait plus confiance au passé
Dans ext/session/session.c, la mise à jour de la progression d'upload est désormais marquée par un drapeau in_rfc1867_update. Pendant cette phase, qui tourne avant que le moindre code utilisateur ne soit autorisé, les objets de la session restent des __PHP_Incomplete_Class : pas d'autoload, pas de __wakeup() ni de __unserialize(). Et l'état de la session est remis à zéro même quand la requête précédente s'est mal terminée.
Le reste du commit, ce sont surtout plus de 200 nouveaux tests oom_*.phpt, qui provoquent volontairement un épuisement mémoire dans à peu près toutes les fonctions sensibles : unserialize(), json_decode(), array_fill(), str_repeat(), le parseur multipart... Un bon exemple de correctif qui ne se contente pas de boucher le trou signalé.
Vous ne pouvez pas mettre à jour tout de suite ?
Soyons clairs : la seule protection complète, c'est la mise à jour, suivie d'un redémarrage de tous les processus PHP. Les avis officiels proposent toutefois des mesures temporaires. Elles réduisent le risque sans le supprimer.
Contre CVE-2026-109618 : memory_limit = -1, mais pas n'importe comment
C'est contre-intuitif, mais c'est bien la mitigation principale de l'avis : sans limite mémoire dans PHP, plus de bailout déclenché par memory_limit. Un échec d'allocation au niveau du système peut toutefois encore emprunter le chemin vulnérable, j'y reviens plus bas. À condition de l'imposer au niveau administrateur, pour qu'aucun pool, répertoire ou code applicatif ne puisse remettre une limite :
; php.ini du PHP web
memory_limit = -1
; pool PHP-FPM
php_admin_value[memory_limit] = -1
# Apache mod_php (VirtualHost, pas .htaccess)
php_admin_value memory_limit -1
Ce réglage a un prix : PHP ne protège plus du tout le serveur contre un script trop gourmand. Il faut donc le compenser ailleurs :
- garder un
post_max_sizeraisonnable, et limiter la taille des requêtes dans le serveur web (client_max_body_sizepour Nginx,LimitRequestBodypour Apache) ; - réduire
max_input_nesting_leveletmax_input_vars; - si l'application n'utilise pas de formulaires avec envoi de fichiers, bloquer les requêtes
multipart/form-dataen amont de PHP ;
Si PHP tourne dans un conteneur, ajoutez impérativement une limite mémoire au conteneur. Sans memory_limit dans PHP, c'est elle qui empêche un script ou une requête malveillante de consommer toute la mémoire de la machine. Selon votre outil :
# Docker
docker run --memory=512m ...
# Docker Compose
services:
php:
mem_limit: 512m
# Kubernetes
resources:
limits:
memory: 512Mi
La valeur est un exemple, à adapter à votre application. Sans conteneur, une limite au niveau du cgroup (memory.max, par exemple via systemd) joue le même rôle. Ce n'est pas pour autant une protection contre la faille : l'avis précise qu'une limite de cgroup ne garantit pas qu'aucune allocation n'échouera. Et quand elle est atteinte, le maître PHP-FPM peut être tué avec tous ses workers s'ils partagent le même conteneur.
Attention, piège : n'utilisez pas RLIMIT_AS ni RLIMIT_DATA (par exemple via ulimit). Ils font échouer mmap() à la limite, ce qui déclenche justement le chemin vulnérable. Même avec memory_limit = -1, un échec d'allocation au niveau du système reste possible : c'est pour ça que la mitigation n'est pas complète.
Contre CVE-2026-109619 : couper la progression d'upload
Le second réglage cible le consommateur le plus direct de la faille :
; php.ini
session.upload_progress.enabled = Off
; pool PHP-FPM
php_admin_flag[session.upload_progress.enabled] = off
# Apache mod_php
php_admin_flag session.upload_progress.enabled Off
Pas de ini_set() ni de .user.ini ici : le réglage doit être appliqué avant le démarrage de la requête. Les fonctionnalités de suivi et d'annulation d'upload qui s'appuient sur ce mécanisme deviennent indisponibles (documentation PHP), ce qui concerne peu de sites. L'avis recommande d'ailleurs de le laisser désactivé même après la mise à jour, si vous ne vous en servez pas.
Sur PHP 8.5, ajoutez fatal_error_backtraces = 0, toujours au niveau administrateur. Et si les performances le permettent, pm.max_requests = 1 dans le pool FPM fait recycler chaque worker après une seule requête, ce qui casse la chaîne entre deux requêtes.
Redémarrer et vérifier
Ces réglages ne valent rien tant que les workers n'ont pas redémarré : rechargez PHP-FPM ou Apache, puis vérifiez la valeur réellement appliquée dans chaque pool, par exemple avec une page phpinfo() temporaire et protégée. Le binaire en ligne de commande (php -v, php -i) ne lit pas forcément le même php.ini que le PHP du serveur web.
Un script pour vérifier votre serveur
Pour gagner du temps, j'ai écrit un petit script shell qui fait un premier inventaire de ces points : php2shell-check.sh. Il signale les réglages à examiner, mais il ne démontre ni l'absence de vulnérabilité, ni que le serveur est exploitable : ses résultats sont à rapprocher des correctifs de votre distribution et de la configuration réellement chargée.
Télécharger le script pour vérifier votre serveur
Il ne modifie rien, il se contente de lire les versions et la configuration. Pour chaque installation de PHP trouvée (CLI, PHP-FPM, y compris les processus en cours d'exécution, et mod_php), il regarde :
- la version, comparée aux versions corrigées de chaque branche, avec le paquet de la distribution s'il y en a un ;
- si la version est sous le correctif, la valeur de
memory_limit, desession.upload_progress.enabledet, sur PHP 8.5, defatal_error_backtracesdans chaque pool PHP-FPM (fichiers inclus compris), en tenant compte des surchargesphp_admin_valueetphp_value; - pour
mod_php, les mêmes réglages dans lephp.iniet chaque surcharge trouvée dans la configuration Apache active, avec son emplacement, puisqu'elle ne s'applique qu'à son VirtualHost ; - la limite mémoire du conteneur, s'il tourne dans Docker ou Kubernetes ;
- sur un hôte Docker, les conteneurs qui embarquent PHP sans limite mémoire.
Récupérez-le directement sur le serveur, lisez-le avant de le lancer (comme tout script trouvé sur Internet), puis exécutez-le :
# Download
wget https://le-programmeur.com/static/neon/php2shell/php2shell-check.sh
less php2shell-check.sh
# On the server
sudo sh php2shell-check.sh
# Inside a container (Alpine images included)
docker exec -i <container> sh < php2shell-check.sh
Attention aux rétroportages : Ubuntu 24.04 corrige PHP2Shell dans le paquet 8.3.6-0ubuntu0.24.04.13, alors que PHP affiche toujours la version 8.3.6 (Ubuntu). Un simple contrôle du numéro de version donnerait donc un faux positif. Le script connaît les paquets corrigés publiés par Ubuntu et Debian, et compare la version du paquet installé pour la version du système :
/usr/sbin/php-fpm8.3 (PHP 8.3.6)
package php8.3-fpm 8.3.6-0ubuntu0.24.04.13
[OK] package php8.3-fpm 8.3.6-0ubuntu0.24.04.13 includes the ubuntu noble fix (>= 8.3.6-0ubuntu0.24.04.13), even though PHP reports 8.3.6
Pour les autres distributions (Red Hat, AlmaLinux, Rocky...), dont les paquets corrigés n'étaient pas encore tous publiés au moment où j'écris, il ne conclut pas : il vous demande de vérifier auprès de la distribution.
Le code de sortie vaut 0 si rien n'a été relevé, 1 s'il reste des points à examiner, et 2 seulement pour une version PHP compilée hors distribution, sous le correctif et sans contournement. De quoi l'intégrer à un outil de supervision ou à Ansible, en gardant ces nuances en tête.
Ses limites, pour être clair. En dehors d'Ubuntu et Debian, il ne connaît pas l'état des rétroportages. Il lit des fichiers : une installation dans un chemin inhabituel, une inclusion exotique ou une surcharge posée ailleurs peuvent lui échapper. Et pour connaître la valeur réellement appliquée par le serveur web, phpinfo() reste la référence.
Et après ?
Mettez à jour PHP dès que possible, sur tous vos serveurs exposés à Internet en priorité. Si vous êtes en hébergement mutualisé, demandez à votre hébergeur quand et en quelle version il applique le correctif. Et pensez à vérifier les versions qui tournent vraiment, pas seulement celles que vous croyez avoir installées !
C'est aussi l'une des raisons pour lesquelles les sites que je construis aujourd'hui avec Søcle servent des pages HTML préparées à l'avance : sans PHP derrière les pages publiques, ce genre de faille ne les concerne pas.
Vous voulez un regard extérieur sur votre infrastructure ou votre code ? Jetez un œil à mon offre d'audit de code, ou entrons en communication !
Sources
- Avis GHSA-w8hw-w9fx-j79w (CVE-2026-109618), php-src
- Avis GHSA-6wmf-4rv5-p3rp (CVE-2026-109619), php-src
- Commit 3584a98 « Avoid state corruption via bailout », php-src
- Journal des modifications de PHP 8
- Annonce du 21 octobre 2026, php.net
- Versions de PHP supportées, php.net
- Progression du téléchargement en session, documentation PHP
- Cloudflare deploys protections against php2shell vulnerability, Cloudflare
- PHP2Shell vulnerability fixes available, Ubuntu
- CVE-2026-109618, suivi de sécurité Debian
- W3Techs : usage de PHP et répartition par version
- W3Techs : usage de WordPress
- ITdaily : PHP2Shell, le bug PHP
- Hardware Cooking : failles PHP critiques, correctifs publiés
- ForgetAboutIT : mitigation, CloudLinux et HardenedPHP