# PHP2Shell : deux bugs de mémoire dans le cycle de vie des requêtes PHP

> Pourquoi un formulaire multipart ou deux requêtes sur le même worker peuvent mener à une RCE dans PHP : mémoire, état global, preuves et correctifs.

Publié le: 2026-11-04  
Canonical: https://rootzero.eu/research/php2shell-deux-bugs-cycle-requete/

**Divulgation et correctifs : 21 octobre 2026. Analyse rédigée le 4 novembre 2026.** « PHP2Shell » désigne deux défauts distincts du moteur PHP, [CVE-2026-109618](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) et [CVE-2026-109619](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp). Le premier concerne la sortie brutale d’une allocation mémoire pendant qu’une requête est partiellement construite. Le second concerne un état global du processus qui survit à la requête alors que la mémoire qu’il désigne a été libérée. L’équipe PHP confirme une exécution de code sans authentification dans certaines configurations, avec les droits du worker web. Les deux avis portent un score CVSS 4.0 de 9,2.

Le point commun n’est ni WordPress ni une fonction PHP dangereuse appelée par le développeur. Il est dans la frontière entre **préparation**, **traitement**, **nettoyage** et **réutilisation** d’un processus PHP. Un correctif applicatif ne remplace donc pas la mise à jour du runtime.

## CVE-2026-109618 : quitter au milieu d’une initialisation

PHP peut interrompre un traitement sur manque de mémoire par un mécanisme apparenté à `longjmp()`. Celui-ci saute plusieurs cadres d’appel. Si le code avait déjà publié un pointeur vers une structure de requête mais n’avait pas fini d’initialiser cette structure, les opérations de nettoyage peuvent ensuite la traiter comme valide. Lire, libérer ou parcourir ce contenu indéterminé crée une corruption mémoire. L’[avis PHP](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) décrit une RCE confirmée sur Apache `mod_php` : ce SAPI exécute le nettoyage même si le démarrage de la requête échoue, avant tout code de l’application.

Pourquoi un client distant peut-il provoquer l’épuisement ? Le parseur `multipart/form-data` peut consommer beaucoup plus de mémoire que la taille du message sur le réseau : nombre non borné d’en-têtes par partie, métadonnées de fichiers vides, noms de champs imbriqués. Une longueur absente ou inutilisable pouvait également passer le premier contrôle `post_max_size`. L’attaquant contrôle la forme de l’entrée et atteint une allocation au mauvais moment. Un OOM ne donne pas mécaniquement une RCE ; l’exploitation dépend de la disposition du tas, de la version, des extensions et de l’enchaînement précis du nettoyage. PHP indique cependant qu’une RCE HTTP complète a été validée sur `mod_php` avec des limites par défaut.

La distinction par environnement compte. Sur PHP-FPM, un échec pendant le démarrage termine le worker, ce qui évite **ce chemin précis** de nettoyage ; des OOM après démarrage restent possibles selon l’application. L’avis ne revendique pas une chaîne RCE FPM complète pour cette CVE seule. Pour FrankenPHP, l’analyse de source suggère une exposition analogue à `mod_php`, sans validation équivalente publiée. ([Portée par SAPI](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w))

## CVE-2026-109619 : un pointeur traverse deux requêtes

Un worker PHP peut servir plusieurs requêtes à la suite. Certaines variables globales du moteur référencent pourtant des objets alloués **pour une seule requête**. Le contrat est simple : remettre ces variables à zéro avant de libérer la mémoire. Le défaut apparaît si une erreur fatale abandonne l’opération avant cette remise à zéro, ou si une étape tardive de nettoyage remplit de nouveau la variable après son nettoyage normal. La requête suivante lit alors un pointeur devenu invalide : c’est un *use after free*.

L’[avis de l’équipe PHP](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp) distingue un **producteur** de l’état périmé et un **consommateur** lors d’une requête ultérieure sur le même worker. Deux chemins sont confirmés : certains diagnostics de configuration émis avant l’initialisation normale, et le mécanisme de suivi de progression des uploads de session. Sur PHP 8.5, `fatal_error_backtraces` fournit un producteur confirmé. Un second upload peut consommer l’ancien état de session sans que l’application appelle elle-même `session_start()`. La RCE du cycle de session a été validée sur `mod_php` et FPM, cette dernière sous condition d’une opération applicative appropriée après démarrage.

Ce modèle explique pourquoi un test avec une seule requête échoue souvent à reproduire le problème : il faut provoquer l’état périmé, conserver le worker, puis lui faire traiter l’entrée consommatrice. Il explique aussi pourquoi masquer les avertissements avec `display_errors=Off` ne protège pas : le moteur accède au pointeur avant de décider d’afficher ou de journaliser le message.

## Comment chercher et vérifier sans confondre plantage et exploitation

Une revue de code part des appels qui peuvent interrompre le démarrage ou le nettoyage, puis inventorie les pointeurs globaux vers des allocations de requête. Chaque chemin d’erreur doit être examiné : l’état est-il remis à zéro, et le worker survit-il ? Les tests pertinents comparent une version vulnérable et une version corrigée, isolées, avec instrumentation mémoire. Un crash, un 502 ou une fuite de mémoire est un signal d’enquête, **pas une preuve de prise de contrôle**. La preuve d’une RCE doit montrer la transition du contenu contrôlé vers l’exécution, avec ses conditions de configuration.

Pour les exploitants, les journaux peuvent aider à trouver des bursts de requêtes multipart, des OOM, des erreurs fatales, des redémarrages de workers et des anomalies autour de `PHP_SESSION_UPLOAD_PROGRESS`. Leur absence ne prouve pas l’absence de tentative. Le trafic peut traverser plusieurs workers, et le point d’échec peut précéder la journalisation PHP.

## Correctifs et mesures d’attente

Les versions [8.2.35, 8.3.36, 8.4.27 et 8.5.12](https://www.php.net/archive/2026.php#2026-10-21-5) contiennent les corrections. Les branches 8.1 et antérieures n’ont pas de nouvelle release PHP officielle ; les distributions peuvent intégrer des *backports*. Il faut vérifier le paquet réellement déployé, pas uniquement la chaîne `php -v`. Redémarrer tous les workers après mise à jour est nécessaire pour éliminer l’ancien état en mémoire.

En attente d’un correctif, l’équipe PHP propose des réductions de surface : désactiver `session.upload_progress.enabled` au niveau administrateur, imposer des limites sur les requêtes multipart et, pour certains chemins OOM, retirer la limite PHP de mémoire par requête tout en conservant une isolation système adaptée. Aucune de ces options ne couvre toutes les variantes ; augmenter ou supprimer `memory_limit` peut aussi dégrader la disponibilité. Le 23 octobre, PHP a ajouté une [recommandation provisoire](https://www.php.net/archive/2026.php#2026-10-23-1) de désactiver le suivi de progression face à un cas distinct, CVE-2026-115560, dépendant d’une combinaison rare de sérialiseurs de session.

La leçon de PHP2Shell tient dans une invariant de durée de vie : un worker réutilisé ne doit jamais transporter un pointeur vers la mémoire d’une requête achevée. La mise à jour corrige ce contrat dans le moteur ; filtrer une seule forme de requête ne suffit pas à le rétablir.

Comparer ces invariants de durée de vie avec [les RCE de compilation Ruby dans GitLab](/research/gitlab-ruby-regexp-rce-2026/) aide à distinguer les mécanismes derrière une même étiquette « RCE ». Notre note [« La preuve avant l’alerte »](/research/preuve-avant-alerte/) détaille la différence entre un indice reproductible, une vulnérabilité confirmée et une conséquence métier démontrée.

**Sources principales :** [avis PHP sur l’OOM](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w), [avis PHP sur l’état global](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp), [annonce de l’équipe PHP](https://www.php.net/archive/2026.php#2026-10-21-5).
