Retour au blog
  • 17 min de lecture

Faille PHP2Shell : mesurer son délai de correctif

Failles critiques PHP2Shell corrigées le 21 octobre 2026 : pour une application legacy, l'enjeu est le délai de correctif. Comment le mesurer et le réduire.

Le 21 octobre 2026, le projet PHP a publié un avis intitulé « Critical remote code execution vulnerabilities, update now » et, le même jour, quatre versions correctives : 8.5.12, 8.4.27, 8.3.36 et 8.2.35. Les deux failles corrigées, que Wiz Research a baptisées PHP2Shell, permettent dans certaines configurations d'exécuter du code à distance sans compte ni action de l'utilisateur. Le code de l'application n'y est pour rien : la faille est dans le moteur.

Pour une équipe dont l'application tourne sur une version supportée, avec une image construite et déployée par un pipeline, la réponse tient en un changement de numéro de version et un déploiement. Pour une application legacy restée en PHP 7.4 ou 8.0, sans tests automatisés et déployée à la main, la même annonce ouvre un chantier. L'écart entre ces deux situations se mesure par un indicateur simple : le délai entre l'annonce d'un correctif et sa mise en production. Cet article part de PHP2Shell pour expliquer comment mesurer ce délai, ce qui l'allonge et comment le réduire, sur PHP comme sur n'importe quel legacy.

En bref

  • Qui est exposé : toute application sur une version de PHP antérieure à 8.2.35, 8.3.36, 8.4.27 ou 8.5.12, quel que soit son code.
  • Action immédiate : mettre à jour PHP. Les réglages de configuration réduisent l'exposition sans la supprimer.
  • Échéance suivante : PHP 8.2 n'aura plus de correctifs de sécurité après le 31 décembre 2026.
  • Indicateur à suivre : le délai entre la publication d'un correctif et sa mise en production.

PHP2Shell : les faits

Deux failles dans le moteur, pas dans le code de l'application

Les deux vulnérabilités sont décrites dans les avis de sécurité du dépôt php-src :

  • CVE-2026-109618, « RCE via OOM corruption » (GHSA-w8hw-w9fx-j79w). Quand une requête épuise la mémoire autorisée, PHP l'interrompt brutalement. Dans certains contextes, cette interruption survient alors que des structures internes ne sont qu'à moitié initialisées, et la fin de requête les manipule quand même. Un attaquant peut provoquer cet épuisement mémoire avec des requêtes "multipart" construites pour consommer bien plus de mémoire dans PHP que leur taille sur le réseau.
  • CVE-2026-109619, « RCE via stale global state » (GHSA-6wmf-4rv5-p3rp). Un état interne lié à une requête (erreur, session, données d'extension) survit à la libération de sa mémoire et reste dans le processus. La requête suivante traitée par le même "worker" relit cet état périmé. L'exploitation suppose donc deux requêtes : l'une qui prépare l'état, l'autre qui le consomme.

Les deux avis attribuent un score de 9,2 en CVSS v4.0. Le vecteur précise la nature du risque : attaque par le réseau, sans privilège ni interaction (AV:N/PR:N/UI:N), mais avec une complexité élevée et des prérequis (AC:H/AT:P). Ce n'est pas une faille exploitable par une simple URL, mais aucun compte n'est nécessaire pour l'atteindre.

Selon Wiz, le défaut remonte à PHP 5.4 et l'exploitation ne dépend pas du code applicatif : une page « Hello World » sur une version vulnérable suffit. Les équipes qui ont trouvé ces failles se sont appuyées sur des modèles d'IA. Si la découverte de failles de ce type s'accélère, les correctifs critiques se multiplient, et la capacité à les appliquer vite devient une compétence permanente plutôt qu'un exercice exceptionnel.

Versions et serveurs touchés

Toutes les versions antérieures à 8.2.35, 8.3.36, 8.4.27 et 8.5.12 sont concernées. Le degré d'exposition dépend de la façon dont PHP est exécuté (la "SAPI") :

  • Apache mod_php : exécution de code à distance confirmée, y compris avec les limites de mémoire et d'entrée par défaut.
  • PHP-FPM : exploitation validée pour CVE-2026-109619 par le chemin « session upload progress ». Cette fonction est activée par défaut et s'exécute même si l'application n'appelle jamais session_start().
  • FrankenPHP, LiteSpeed (LSAPI), php-cgi : également dans le périmètre. Pour FrankenPHP et LSAPI, les avis concluent à une vulnérabilité probable par analyse du code source, sans exploit complet démontré, et précisent que cela ne vaut pas immunité.

Concrètement, la quasi-totalité des déploiements PHP en production est dans le périmètre, du site WordPress à l'application métier Symfony ou Laravel.

Les failles sont-elles exploitées ?

L'exposition a commencé avant le correctif. Le 20 octobre, la veille de la publication, les identifiants CVE des deux failles ont été divulgués sur le forum Hacker News, selon Infosecurity Magazine. Des tentatives d'exploitation ont été observées dès le jour de la publication par plusieurs acteurs (watchTowr, Patchstack, Cloudflare), et Amazon indiquait au 26 octobre 2026 en avoir vu dès quatre jours avant la sortie des correctifs, d'après SecurityWeek. Des preuves de concept circulent. Au 26 octobre 2026, aucune compromission réussie n'a été confirmée publiquement. Pour une décision de priorité, cela suffit : une faille critique, exploitable sans compte, avec du code d'attaque public, se traite comme une urgence.

Pourquoi les contournements par configuration ne suffisent pas

Face à une faille, le premier réflexe est souvent de chercher un réglage qui la neutralise en attendant la mise à jour. Les avis en proposent plusieurs, et tous sont présentés comme partiels :

  • session.upload_progress.enabled = 0, posé dans php.ini (pas via ini_set() ni un .user.ini FPM) puis suivi d'un redémarrage de tous les "workers" : bloque les chemins de session confirmés, pas les autres variantes ;
  • fatal_error_backtraces = 0 sur PHP 8.5 : supprime un déclencheur confirmé de CVE-2026-109619, pas les autres états périmés ;
  • pm.max_requests = 1 sur FPM : un "worker" ne traite qu'une requête, ce qui empêche l'état périmé de passer à la suivante, au prix d'un coût de performance important ;
  • memory_limit = -1 : retire la limite mémoire que l'attaquant pilote, mais une allocation peut encore échouer au niveau système et atteindre le chemin vulnérable (d'où l'avertissement de l'avis contre RLIMIT_AS et RLIMIT_DATA).

Les notes de version résument la position du projet en une phrase : « There are no complete mitigations, updating is the only safe choice ». Ces réglages servent à réduire l'exposition pendant quelques jours, le temps de mettre à jour. Ils ne remplacent pas la mise à jour, et chacun a un coût (performance, fonctionnalité d'upload désactivée) qu'il faut lui-même tester avant de l'appliquer en production.

L'avis du 23 octobre : un correctif ne clôt pas le sujet

Deux jours après les correctifs, le projet PHP a publié un avis informatif sur une troisième faille, CVE-2026-115560, toujours dans « session upload progress ». Elle touche aussi les versions corrigées du 21 octobre, 8.5.12 comprise, et ne sera corrigée qu'à la prochaine version. Son exploitation suppose une application qui change de format de sérialisation des sessions (session.serialize_handler) en cours d'exécution, ce qui est rare.

L'enseignement dépasse ce cas précis. Une faille publiée attire l'attention des chercheurs sur le même code, et des variantes suivent souvent dans les semaines qui viennent. Une équipe qui a mis deux semaines à appliquer le premier correctif devra recommencer pour le suivant. Le délai de correctif n'est pas une performance ponctuelle, c'est une capacité qu'on exerce plusieurs fois de suite.

PHP 8.1 ou plus ancien, et la fin de support de PHP 8.2

Les branches supportées sont 8.2 à 8.5 (calendrier officiel). PHP 8.1 et les versions antérieures ne reçoivent plus de mises à jour du projet. Vu la gravité des failles, l'équipe de sécurité de PHP a tout de même publié des correctifs minimaux pour 7.4, 8.0 et 8.1, sous forme de modifications du code source, destinés à ceux qui compilent PHP eux-mêmes ou n'ont pas de fournisseur de support étendu. Ce geste est exceptionnel : rien ne garantit qu'il se répétera à la prochaine faille.

Pour une application sur 8.1 ou plus ancien, trois options existent, et seule la dernière règle le problème :

  1. Appliquer le correctif minimal en recompilant PHP. Cela suppose de savoir reconstruire son PHP, ce qui est rarement le cas d'une équipe qui utilise les paquets d'une distribution.
  2. S'appuyer sur un support étendu (distribution Linux d'entreprise, fournisseur de support à long terme). C'est un contrat, avec un coût récurrent, qui achète du temps sans réduire la dette.
  3. Monter vers une branche supportée. C'est la seule option qui remet l'application dans le flux normal des correctifs.

PHP 8.2 sort lui-même du support de sécurité le 31 décembre 2026, soit un peu plus de deux mois après PHP2Shell. Une application sur 8.2 qui vient d'être corrigée en 8.2.35 sera, au 1er janvier, dans la situation de celles qui sont aujourd'hui en 8.1. Viser 8.4 (supportée jusqu'à fin 2028) ou 8.5 (fin 2029) évite de refaire la même migration dans un an.

Le délai de correctif, un indicateur à mesurer

Définition

On appelle ici délai de correctif le temps écoulé entre la publication d'un correctif de sécurité par l'éditeur et le moment où la version corrigée sert effectivement le trafic de production. Ce n'est pas le temps de développement, ni le temps jusqu'à la fin des tests : c'est le temps pendant lequel la production reste exposée alors que la correction existe.

L'indicateur a deux qualités pour un décideur. Il se mesure sans expertise technique (deux dates), et il résume à lui seul l'état de l'outillage : inventaire, tests, pipeline, déploiement, supervision. Un délai court ne s'obtient pas sans ces éléments.

Chaîne en six étapes, de l'annonce d'un correctif à sa vérification en production : annonce, qualification, construction, tests, déploiement, vérification, avec sous chacune le frein qui l'allonge

Chaque étape a son frein typique. Le délai réel est la somme des six, attentes comprises.

Comment mesurer le délai de correctif

La date de début est publique : pour PHP, la page ChangeLog et l'API des versions de php.net la donnent. La date de fin doit être relevée en production, pas dans le ticket : c'est le moment où la version qui tourne réellement change.

Un contrôle automatisé, lancé chaque jour par la supervision ou un job planifié, suffit à rendre l'écart visible :

sh
#!/bin/sh
# Compare la version PHP réellement servie en production
# à la dernière version publiée de sa branche.
set -eu
 
running=$(php -r 'echo PHP_VERSION;')            # à exécuter dans le conteneur de production
branch=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
 
latest=$(curl -fsS "https://www.php.net/releases/?json&version=${branch}" \
  | php -r '$d = json_decode(stream_get_contents(STDIN), true); echo $d["version"] ?? "";')
 
if [ -z "$latest" ]; then
  echo "Branche ${branch} inconnue ou hors support : mise à jour de branche nécessaire" >&2
  exit 2
fi
 
if [ "$running" != "$latest" ]; then
  echo "PHP ${running} en production, ${latest} disponible" >&2
  exit 1
fi
 
echo "PHP ${running} à jour"

La même réponse donne la date de publication et les branches supportées (supported_versions), de quoi horodater le début du délai et repérer une branche sortie du support.

Une fois les deux dates connues, deux chiffres suffisent :

  • le délai observé sur les derniers correctifs critiques (PHP2Shell en est un) ;
  • le délai cible, décidé par la direction en fonction du risque : par exemple quelques jours pour un correctif critique exploitable sans authentification, un cycle de livraison normal pour le reste.

Ce qui allonge le délai

Les causes se retrouvent d'un legacy à l'autre, quelle que soit la technologie :

  • Pas d'inventaire. Personne ne sait avec certitude quelle version de PHP tourne sur quel serveur, ni quelles extensions sont chargées. La qualification prend des jours au lieu d'une heure.
  • Une version hors support. Le correctif n'existe pas pour la branche utilisée : la mise à jour de sécurité devient une migration de version majeure, avec ses incompatibilités.
  • Pas de tests automatisés. Chaque mise à jour se valide par des tests manuels, longs et incomplets. Faute de filet, on repousse.
  • Un déploiement manuel. La mise en production demande une personne précise, une fenêtre de maintenance, une procédure documentée ou non. Les fenêtres sont rares, donc les mises à jour s'accumulent.
  • Des dépendances figées. Une bibliothèque ou un framework ancien empêche de monter PHP : il faut d'abord le mettre à jour, parfois le remplacer.

Retour d'expérience : pourquoi les applications legacy restent bloquées

Les applications qui restent sur d'anciennes versions ne le sont presque jamais par choix. Le blocage vient d'une boucle : sans tests, une mise à jour est risquée ; parce qu'elle est risquée, on la repousse ; plus on la repousse, plus l'écart de versions grandit et plus la mise à jour devient risquée. L'absence de pipeline de déploiement ajoute une seconde boucle : chaque mise en production coûte cher, donc on en fait peu, donc chacune embarque beaucoup de changements et devient encore plus coûteuse à valider.

Dans ces situations, le problème n'est pas PHP. Une application Python figée sur une version en fin de vie, un batch COBOL qu'on n'ose plus recompiler ou un frontend bloqué sur une ancienne version de son framework présentent la même mécanique. Les mêmes leviers s'appliquent.

Ce qui réduit le délai de correctif

Trois pratiques ont fait leurs preuves pour sortir de la boucle. Aucune n'exige de réécrire l'application.

Monter de version PHP par étapes

Sauter de PHP 7.4 à 8.4 d'un coup cumule quatre lots d'incompatibilités, et il devient impossible de savoir laquelle a cassé quoi. Une montée par étapes, une version mineure après l'autre, chacune déployée en production avant de passer à la suivante, garde chaque changement petit et réversible.

L'outillage aide beaucoup. Rector réécrit automatiquement le code pour une version cible ; sa documentation recommande, sur un projet existant, de monter par niveaux plutôt que d'appliquer d'un coup l'ensemble des règles :

php
<?php
 
use Rector\Config\RectorConfig;
 
return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    // Lit la contrainte "php" de composer.json : on la relève d'une version à chaque étape.
    ->withPhpSets();

L'analyse statique (PHPStan, Psalm) complète Rector en signalant les appels à des fonctions retirées et les changements de types. L'ordre compte : d'abord les dépendances Composer compatibles avec la version cible, puis le code, puis le moteur.

Quand l'application est trop imbriquée pour être mise à jour par morceaux, la question devient celle de son découpage interne. Un monolithe modulaire aux frontières explicites se met à jour et se teste module par module, sans passer par des microservices.

Écrire des tests de caractérisation

Une application legacy a rarement des spécifications à jour. Ses règles métier sont dans le code, y compris ses bizarreries dont dépendent peut-être des clients. Les tests de caractérisation ne vérifient pas que le code est juste : ils figent ce qu'il fait aujourd'hui, pour détecter tout changement de comportement après une mise à jour.

La méthode est mécanique :

  1. Choisir les points d'entrée qui portent le plus de valeur ou de risque (calcul de facture, export comptable, API publique).
  2. Enregistrer des entrées réelles ou représentatives, anonymisées, et les sorties produites par la version actuelle.
  3. Écrire un test qui rejoue chaque entrée et compare la sortie à celle enregistrée.
php
<?php
 
use PHPUnit\Framework\TestCase;
 
final class InvoiceCharacterizationTest extends TestCase
{
    public static function recordedCases(): iterable
    {
        foreach (glob(__DIR__ . '/fixtures/invoices/*.json') as $file) {
            yield basename($file) => [$file];
        }
    }
 
    /**
     * Annotation plutôt qu'attribut #[DataProvider] (PHPUnit 10+, donc PHP 8.1+) :
     * le test doit tourner sur l'ancienne version, avant la montée de version.
     *
     * @dataProvider recordedCases
     */
    public function testOutputMatchesRecordedBehaviour(string $file): void
    {
        $case = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
 
        $actual = legacy_compute_invoice($case['order']);
 
        // La valeur attendue vient de la version actuelle, bugs compris :
        // on corrige un comportement après la migration, jamais pendant.
        $this->assertSame($case['expected'], $actual);
    }
}

Les cas limites méritent une attention particulière : arrondis de flottants, comparaisons entre chaînes et nombres (dont le comportement a changé en PHP 8.0), dates et fuseaux horaires, encodages. C'est là que les montées de version modifient le comportement sans lever d'erreur.

Automatiser le déploiement

Le dernier levier transforme une mise à jour de sécurité en opération banale. Le principe : la version de PHP est déclarée dans le code, pas installée à la main sur un serveur.

dockerfile
# Version exacte : une mise à jour de sécurité est un changement de cette ligne,
# relu, testé et déployé comme n'importe quel commit.
FROM php:8.4.27-fpm-bookworm

Un outil de mise à jour des dépendances (Renovate, Dependabot) ouvre automatiquement une demande de fusion quand une nouvelle image est publiée. Le pipeline construit l'image, lance les tests de caractérisation et l'analyse statique, puis déploie. Avec cette chaîne en place, un correctif critique comme PHP2Shell devient une opération courte, dont l'essentiel est de la relecture.

C'est aussi ce qui rend l'avis du 23 octobre gérable : redéployer une deuxième fois la même semaine ne coûte rien de plus que la première. La modernisation progressive d'un existant repose sur la même chaîne : sans déploiement automatisé, chaque étape de migration redevient un projet.

Traiter la dette de mise à jour comme un risque de sécurité

Dans beaucoup d'équipes, la mise à jour des versions est rangée dans la dette technique, à côté du renommage de variables et du nettoyage de code mort. Elle passe donc après les fonctionnalités, au titre du confort des développeurs. PHP2Shell montre que cette classification est fausse : une version non mise à jour est une exposition, avec une probabilité et un impact, comme un serveur mal configuré.

Pour qu'une direction puisse arbitrer, la dette de mise à jour doit être présentée dans ses termes :

  • L'exposition actuelle. Quelles applications tournent sur une version hors support ou bientôt hors support (PHP 8.2 au 31 décembre 2026), lesquelles sont accessibles depuis Internet, quelles données elles manipulent.
  • Le délai de correctif observé, mesuré sur PHP2Shell, comparé au délai cible.
  • Le coût de l'inaction, exprimé en scénarios : interruption de service, fuite de données, notification à la CNIL et aux clients, obligations contractuelles de sécurité envers les clients.
  • Le coût de la remise à niveau, découpé en étapes livrables (inventaire, tests de caractérisation sur les flux critiques, pipeline, montées de version), chacune avec son effet mesurable sur le délai.

Présentée ainsi, la mise à jour n'est plus une demande de l'équipe technique contre la feuille de route produit : c'est un risque inscrit au même registre que les autres, que la direction accepte, réduit ou transfère en connaissance de cause. Le même raisonnement s'applique aux autres écosystèmes : chaque version de Python, par exemple, reçoit environ cinq ans de correctifs, et une nouvelle version comme Python 3.15 rappelle que l'horloge tourne pour les plus anciennes. Sur des systèmes COBOL, où les cycles sont plus longs, la modernisation progressive suit la même logique de petits pas testés.

Les questions à se poser cette semaine

Pour un CTO ou un tech lead, PHP2Shell est une occasion de mesurer plutôt que de supposer :

  1. Toutes nos applications PHP en production tournent-elles désormais sur 8.2.35, 8.3.36, 8.4.27, 8.5.12 ou une version ultérieure ? Comment le sait-on, et en combien de temps a-t-on pu répondre à cette question ?
  2. Combien de temps s'est écoulé entre le 21 octobre et la mise en production du correctif, application par application ?
  3. Lesquelles sont sur PHP 8.2 ou plus ancien, et quel est le plan avant le 31 décembre 2026 ?
  4. Si un nouveau correctif sort la semaine prochaine, qu'est-ce qui nous empêcherait de le déployer en une journée : l'inventaire, les tests, le pipeline ou une dépendance figée ?
  5. La direction connaît-elle notre délai de correctif, et a-t-elle fixé un délai cible ?

Si ces questions révèlent un écart important, un regard extérieur aide souvent à prioriser : c'est l'objet d'un audit et d'un accompagnement en conseil logiciel, qui part de l'existant pour définir les étapes et le délai cible.

Et vous, quel est votre délai réel pour appliquer un correctif PHP critique ?

Un sujet à creuser pour votre projet ?

Discutons de votre contexte technique et de vos enjeux.

Parlons de votre projet