Image principale de l'article PHP2Shell : deux failles critiques, un embargo qui saute et un Web entier à patcher

PHP2Shell : deux failles critiques, un embargo qui saute et un Web entier à patcher

Publié le 22 October 2026 à 21:04 par Flo

Mardi soir, PHP a perdu le contrôle du calendrier

D'habitude, une faille critique dans un langage aussi répandu que PHP suit un rituel bien rodé. Un chercheur la trouve et la signale en privé, l'équipe sécurité prépare un correctif, les distributions le reçoivent en avance, et tout le monde publie le même jour à la même heure. Les attaquants découvrent la faille en même temps que le patch. C'est imparfait, mais ça laisse aux admins une vraie chance.

Cette semaine, le rituel a cassé. Le 20 octobre, les identifiants de deux CVE critiques, CVE-2026-109618 et CVE-2026-109619, ont commencé à circuler sur les forums et les réseaux sociaux près de deux jours avant la date prévue. Le surnom a suivi dans la foulée : PHP2Shell. Le correctif est sorti le 21 octobre, en avance sur le planning, mais des attaques avaient déjà été repérées le matin même et un outil d'exploitation est désormais public.

Si vous avez un serveur PHP exposé sur Internet, la suite vous concerne directement. Et si vous comptez sur votre framework pour vous protéger, mauvaise nouvelle : le problème se trouve sous votre framework.

Deux use-after-free au coeur du moteur

Les deux failles touchent ce que presque chaque requête traverse : la gestion mémoire et le traitement des erreurs fatales. L'équipe sécurité de PHP les note 9,2 en CVSS v4.0, et le NVD les a classées 9,8 en CVSS v3.1. Le mécanisme commun tient en deux phrases. Une erreur fatale, souvent un dépassement de memory_limit, interrompt le code natif de PHP avant qu'il ait fait le ménage. Une requête suivante, traitée par le même worker, réutilise alors une mémoire déjà libérée comme si elle était valide.

  • CVE-2026-109618, corruption après un out-of-memory. Si une requête atteint memory_limit au mauvais moment, PHP laisse derrière lui des structures à moitié initialisées. Le chemin d'attaque passe par le gestionnaire de session upload progress, qui désérialise des données pendant l'upload. Avec Apache et mod_php, une requête non authentifiée déclenche le bug avant même que votre code ne s'exécute, et l'exécution de code à distance a été démontrée en configuration par défaut. Sur PHP-FPM, l'équipe PHP a confirmé une RCE via des chemins applicatifs.
  • CVE-2026-109619, l'état périmé. Une erreur fatale au mauvais endroit laisse un état interne qui pointe vers la mémoire de la requête précédente. Sansec y décrit une confusion de type liée à OPcache. Le chercheur a démontré une RCE sur une application réelle tournant sous mod_php.

Le premier bug a un effet secondaire particulièrement désagréable : il fait fuir de la mémoire, façon Heartbleed. Le gestionnaire d'upload progress peut renvoyer au client, dans le cookie PHPSESSID, des morceaux de mémoire libérée du worker. On peut y trouver des mots de passe, des jetons de session, des clés d'API, voire des clés privées TLS venant d'autres requêtes. Comme ce gestionnaire tourne avant l'application, l'attaque fonctionne même si votre code n'appelle jamais session_start().

Dernier détail qui donne le vertige : le code vulnérable date de PHP 5.4.0, sorti en 2012. Tous les déclencheurs connus passent par une requête multipart/form-data, le format des uploads de fichiers, que PHP parse avant de rendre la main à votre code.

Une divulgation coordonnée qui a déraillé à 48 heures de l'arrivée

Le plus frustrant, c'est que le problème n'était pas inconnu. En 2024, PHP a subi son premier audit de sécurité externe depuis des années, mené par Quarkslab via l'OSTIF et financé par la Sovereign Tech Agency allemande. En corrigeant un des points remontés, un développeur du core a repéré les premiers signes du problème d'OOM. À l'époque, aucun vecteur d'exploitation concret n'était connu. L'équipe a imaginé plusieurs correctifs concurrents, chacun avec son lot de compromis (casse de compatibilité, nouveau risque de déni de service, modifications imposées à de nombreuses extensions tierces), et le travail a calé puis redémarré pendant des mois. La PHP Foundation ne se cherche pas d'excuses dans son billet : elle parle de "structural failure", liée à un mode de décision au consensus et au manque de moyens.

Tout s'accélère à l'été 2026. Calif commence un audit le 30 juillet. Le 11 août, Ryan A, chercheur chez Calif, signale une RCE inconditionnelle avec la même cause racine, et OpenAI rapporte la même chose le même jour. Wiz suit le 24 août. À partir de là, le correctif devient la priorité de toute l'équipe sécurité financée par la Fondation. La sortie est calée au jeudi 22 octobre, 14h00 UTC, avec les distributions et les fournisseurs LTS dans la boucle pour qu'ils aient des paquets prêts le jour J.

Puis la chronologie s'emballe :

  1. Mardi 20 octobre, vers 15h00 UTC. Les deux identifiants fuitent sur des forums et les réseaux sociaux, avec une mauvaise heure de sortie et un score CVSS erroné de 10. Aucun détail technique, mais l'information est lâchée. Un post Hacker News, retiré depuis, circule en captures d'écran.
  2. 19h30 UTC. PHP avance sa pré-annonce et déplace la sortie au mercredi 21 octobre, 14h00 UTC. Publier immédiatement aurait laissé sans paquet tous ceux qui dépendent de leur distribution.
  3. Vers 20h00 UTC. Calif publie par erreur une vidéo de son proof of concept en action sur plusieurs cibles. Elle est supprimée, mais des copies tournent et accélèrent l'écriture de PoC publics.
  4. Mercredi 21 octobre, 07h41 UTC. Chainguard rompt l'embargo et publie des images contenant le correctif, plus de six heures avant l'heure convenue. N'importe qui peut alors étudier le patch sous forme binaire.
  5. 07h43 UTC. Cloudflare signale les premières attaques.
  6. 14h00 UTC. Sortie de PHP 8.5.12, 8.4.27, 8.3.36 et 8.2.35. Calif, Wiz et OpenAI publient leurs analyses.

Le principe d'un embargo, c'est que personne ne connaisse la faille avant que le patch soit disponible. Cette fois, les attaquants ont eu plusieurs heures d'avance sur la plupart des défenseurs. Aux Pays-Bas, plusieurs sites, dont Tweakers, la Chambre des représentants et le NCSC, ont préféré se mettre hors ligne par précaution. On peut débattre des responsabilités de chacun, mais le résultat est là : pour une fois, ce sont les admins qui ont couru derrière.

Qui est exposé ? À peu près tout le monde

PHP fait tourner une part énorme du Web, souvent estimée autour de sept sites sur dix en comptant WordPress. Mais le chiffre qui compte ici, c'est la liste des applications sur lesquelles Calif a démontré une exécution de code sans authentification : Magento 2.4.9, WordPress 7.1.2, Drupal 11.4.3, Joomla 6.1.4, MediaWiki, Roundcube, phpMyAdmin, Nextcloud, Laravel 13.11, Symfony 8.1 et WHMCS. Le CMS, le framework ou la qualité de votre code n'y changent rien. La faille est dans l'interpréteur.

Le niveau de risque varie quand même selon la façon dont PHP est déployé :

ContexteExpositionPourquoi
Apache + mod_phpCritiqueRCE démontrée sans authentification, avant tout code applicatif, en configuration par défaut
Nginx ou Caddy + PHP-FPMÉlevéeRCE confirmée via des chemins applicatifs, fuite mémoire possible
Hébergement mutualiséÉlevéeVous dépendez du calendrier de l'hébergeur, souvent sur des branches anciennes
Appliances et logiciels qui embarquent PHPÉlevée et opaqueIl faut attendre un nouveau firmware du fabricant
PHP 8.1 et antérieurCritique et durablePas de release officielle, seulement des patchs minimaux
Scripts CLI sans exposition HTTPFaible a prioriLes déclencheurs connus passent par une requête multipart

La fuite mémoire mérite une attention à part. Même sans RCE réussie, un attaquant qui récupère des fragments de mémoire peut repartir avec des secrets. Si votre serveur est resté exposé entre mardi et le moment du patch, faire tourner vos clés d'API, vos secrets applicatifs (APP_SECRET chez Symfony) et invalider les sessions n'a rien d'excessif. C'est fastidieux, mais beaucoup moins qu'une compromission découverte trois mois plus tard.

Si vous ne pouvez pas patcher dans l'heure

Soyons clairs : la seule vraie correction, c'est la mise à jour de PHP. Rien dans votre code applicatif ne peut corriger PHP2Shell. Les mesures suivantes font gagner quelques heures, et elles restent partielles.

  • Bloquer les requêtes multipart au niveau du serveur web. Tous les déclencheurs connus passent par multipart/form-data. Le blocage casse les uploads, et certains sites envoient leurs formulaires de panier ou de paiement en multipart, alors testez avant de partir en week-end.
  • Passer le site en lecture seule en refusant tout ce qui n'est pas GET ou HEAD, ou le couper derrière une page statique. Les modes maintenance de WordPress ou Magento ne servent à rien ici : PHP parse la requête avant que l'application ne regarde le flag.
  • Désactiver session.upload_progress.enabled. Gina Peter Banyard, du core PHP, a prévenu que ça ne suffit pas seul. Calif suggère aussi memory_limit = -1, mais une seule requête peut alors avaler toute la RAM du serveur : à surveiller de près, et à annuler après le patch.

Avec PHP-FPM, le pool lit sa propre configuration, pas le php.ini de la CLI. Ajoutez php_admin_flag[session.upload_progress.enabled] = off dans chaque pool, rechargez FPM, et vérifiez la valeur dans une requête servie par FPM plutôt qu'avec php -i. Côté Nginx, voici les deux règles d'urgence :

# Mitigation temporaire PHP2Shell, à retirer après la mise à jour de PHP
# À placer dans chaque bloc server qui transmet les requêtes à PHP

# 1. Refuser les requêtes multipart (casse les envois de fichiers)
if ($http_content_type ~* "multipart/form-data") {
    return 403;
}

# 2. Variante plus radicale : site en lecture seule
# if ($request_method !~ ^(GET|HEAD)$) {
#     return 503;
# }

# Puis : nginx -t && systemctl reload nginx

Corriger sur un serveur classique

Les versions corrigées sont PHP 8.5.12, 8.4.27, 8.3.36 et 8.2.35. La règle de base : mettre à jour depuis la source d'où vient déjà votre PHP. Distribution, PPA d'Ondřej Surý, dépôt de Remi, image Docker ou FrankenPHP, chacun a son propre calendrier.

Mattias Geniar, qui a testé toutes ses commandes en conteneur le jour de la sortie, relève plusieurs pièges :

  • Ubuntu et Red Hat rétroportent souvent le correctif sans changer le numéro de version. Ubuntu 24.04 affiche toujours 8.3.6. C'est la version du paquet qu'il faut regarder, pas php -v. Debian livre au contraire la nouvelle version upstream.
  • Le dépôt de Remi n'avait pas le correctif le 21 octobre. La PHP Foundation conseille de basculer sur les paquets de la distribution ou de compiler depuis les sources.
  • Sur RHEL 8, un simple dnf install php installe encore PHP 7.2, un flux de modules retiré depuis 2021 et qui ne reçoit plus rien.
  • Avec mod_php, un reload ou un apachectl graceful ne suffit pas. Apache garde l'ancienne libphp en mémoire, il faut un vrai redémarrage. Côté FPM, un reload charge bien le nouveau binaire.

Pensez aussi à tout ce qui tourne longtemps : workers de queue, jobs Supervisor, Laravel Octane. Un processus garde l'ancien PHP jusqu'à son redémarrage. Avec Symfony Messenger, php bin/console messenger:stop-workers demande aux workers de s'arrêter proprement, et Supervisor les relance sur le nouveau binaire.

#!/usr/bin/env bash
# Debian / Ubuntu (dépôts officiels, PPA ondrej, packages.sury.org)
# Remplacez la branche, et répétez pour chaque version de PHP installée
set -euo pipefail

PHP_BRANCH="8.3"

# 1. Inventaire des paquets PHP installés et de leurs versions
dpkg -l 'php*' | awk '/^ii/{print $2, $3}'

# 2. Mise à jour ciblée des seuls paquets PHP (n'installe rien de nouveau)
sudo apt-get update
sudo apt-get install --only-upgrade "php${PHP_BRANCH}*" "libapache2-mod-php${PHP_BRANCH}"

# 3. Sur Ubuntu, le changelog du paquet cite les CVE corrigées
zgrep -E 'CVE-2026-10961[89]' \
  "/usr/share/doc/php${PHP_BRANCH}-fpm/changelog.Debian.gz" || true

# 4. Redémarrages
sudo systemctl restart "php${PHP_BRANCH}-fpm"
# mod_php : redémarrage complet obligatoire, pas de graceful
# sudo systemctl restart apache2

# 5. Aucun processus ne doit encore mapper l'ancien binaire supprimé
sudo grep -lE ' /(usr|lib|opt).*\(deleted\)$' /proc/[0-9]*/maps \
  || echo "OK : plus aucun ancien PHP en mémoire"

# RHEL / AlmaLinux / Rocky :
#   sudo dnf upgrade 'php*' && sudo systemctl restart php-fpm
#   dnf updateinfo list --security | grep php

Corriger sous Docker

Sous Docker, la tentation est de croire qu'un docker compose restart suffit. Ce n'est pas le cas : le conteneur relance la même image, avec le même PHP vulnérable. Il faut récupérer la nouvelle image de base, reconstruire vos propres images avec --pull, puis recréer les conteneurs.

Deux habitudes à prendre au passage. D'abord, épingler la version de patch dans le Dockerfile (php:8.4.27-fpm plutôt que php:8.4-fpm). Un tag flottant vous met à jour en théorie, mais vous ne savez jamais ce qui tourne vraiment, et le cache de build peut vous resservir une vieille couche. Vérifiez sur Docker Hub que le tag corrigé existe bien pour votre variante (Alpine, Debian, FPM, Apache) avant de déployer. Ensuite, n'oubliez pas les conteneurs annexes qui embarquent eux aussi PHP : workers Messenger, cron, conteneur de migrations. Avec FrankenPHP, PHP est compilé dans le binaire, donc il faut une nouvelle release ou une nouvelle image : ni apt ni dnf ne vous aideront.

Le Dockerfile ci-dessous ajoute aussi un petit durcissement que Geniar conseille de garder même après le patch : l'upload progress des sessions désactivé (rares sont les applications qui s'en servent) et session.use_strict_mode activé, comme PHP le recommande pour tous les sites.

# syntax=docker/dockerfile:1

# Version de patch épinglée : corrige CVE-2026-109618 et CVE-2026-109619
FROM php:8.4.27-fpm

# Durcissement : l'upload progress des sessions est au coeur
# de CVE-2026-109618 et presque aucune application ne s'en sert
RUN { \
      echo 'session.upload_progress.enabled = 0'; \
      echo 'session.use_strict_mode = 1'; \
    } > "$PHP_INI_DIR/conf.d/zz-hardening.ini"

COPY --chown=www-data:www-data . /var/www/html

# Reconstruction et redéploiement :
#   docker compose build --pull
#   docker compose up -d --force-recreate
#   docker compose exec php php -v

Vérifier plutôt que supposer

Entre les rétroportages qui ne changent pas le numéro de version, les conteneurs oubliés et les workers jamais redémarrés, on peut facilement se croire à jour sans l'être. Un contrôle exécuté par le SAPI qui sert réellement vos requêtes, et non par la CLI, évite les mauvaises surprises. Le script ci-dessous peut servir de route de healthcheck interne ou d'étape de CI.

Il a une limite assumée : sur Ubuntu ou RHEL, une version rétroportée apparaîtra comme vulnérable alors qu'elle est corrigée. Dans ce cas, c'est le changelog du paquet qui fait foi. Et ne laissez pas cette route accessible publiquement, elle donne à un attaquant exactement l'information qu'il cherche.

<?php

declare(strict_types=1);

/**
 * Contrôle PHP2Shell (CVE-2026-109618 / CVE-2026-109619).
 * À exécuter via le SAPI de production (FPM, mod_php), pas en CLI.
 * Faux positif possible sur les distributions qui rétroportent
 * sans changer la version (Ubuntu, RHEL) : vérifier le paquet.
 */
final class Php2ShellCheck
{
    /** Première version corrigée pour chaque branche supportée */
    private const FIXED = [
        '8.5' => '8.5.12',
        '8.4' => '8.4.27',
        '8.3' => '8.3.36',
        '8.2' => '8.2.35',
    ];

    /**
     * @return array{sapi: string, version: string, status: string, upload_progress: bool}
     */
    public static function run(): array
    {
        $branch = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
        $fixed = self::FIXED[$branch] ?? null;

        $status = match (true) {
            $fixed === null => 'branche non supportée : migrer',
            version_compare(PHP_VERSION, $fixed, '>=') => 'corrigé',
            default => sprintf('VULNÉRABLE : passer en %s', $fixed),
        };

        return [
            'sapi' => PHP_SAPI,
            'version' => PHP_VERSION,
            'status' => $status,
            'upload_progress' => (bool) ini_get('session.upload_progress.enabled'),
        ];
    }
}

header('Content-Type: application/json');
echo json_encode(Php2ShellCheck::run(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

PHP 8.1 et antérieur : les correctifs hors du circuit officiel

C'est le point le plus délicat. PHP ne publie de release que pour les branches supportées, donc rien d'officiel pour 8.1, 8.0, 7.4 et en dessous. Google l'écrit noir sur blanc pour App Engine : les runtimes PHP 5.5, 7.2, 7.3, 7.4 et 8.1 restent vulnérables et doivent migrer. Plusieurs filets existent quand même :

  • Les patchs minimaux de l'équipe PHP. L'équipe sécurité a publié de petits patchs source pour 7.4, 8.0 et 8.1, présentés comme une mitigation minimale pour les versions en fin de vie. Ils visent ceux qui compilent PHP eux-mêmes ou n'ont aucun fournisseur pour rétroporter. Beaucoup plus légers que le vrai correctif, ils désactivent complètement l'upload progress des sessions, quoi que dise votre php.ini.
  • Les paquets d'Ondřej Surý. Le paquet PHP 7.4 de packages.sury.org intègre ce même patch.
  • Les distributions à support étendu. Debian 11 (PHP 7.4) n'a le correctif que via l'ELTS payant de Freexian, le LTS Debian ayant pris fin le 31 août 2026. Ubuntu 20.04 et 18.04 passent par Ubuntu Pro (ESM Infra), et Ubuntu 22.04, qui livre PHP 8.1, dépend d'un rétroportage de Canonical.
  • Les offres commerciales. CloudLinux prépare des paquets alt-php corrigés. TuxCare vend un support étendu qui rétroporte les CVE sur les branches 5.6 à 8.1 ; vérifiez dans son tracker que ces deux CVE sont couvertes pour votre version.
  • Le dépôt de Remi continue de patcher 7.4, 8.0 et 8.1, sans engagement.

Mon avis : ces correctifs sont des bouées, pas des bateaux. Un rétroportage corrige ce bug-là, pas le suivant, et plus personne ne relit ces branches. Si vous planifiez une migration, sautez PHP 8.2 : son support sécurité s'arrête le 31 décembre 2026, dans à peine plus de deux mois. Visez directement 8.4 ou 8.5.

Ce que PHP2Shell dit de notre écosystème

On pourrait retenir le chaos de la divulgation, la vidéo publiée par erreur, l'embargo rompu par Chainguard. Ce sont de vraies fautes. Mais le fond du problème est ailleurs : une faiblesse repérée dès 2024 n'a été corrigée qu'en octobre 2026, faute d'assez d'ingénieurs capables de trancher un choix difficile au coeur du moteur. La Fondation le reconnaît elle-même et a commencé à changer ses processus, avec une équipe dédiée à la sécurité de l'écosystème créée en mai et de nouvelles recrues pour le triage et le durcissement.

L'autre leçon est plus inconfortable. Les outils d'audit, IA comprise, trouvent désormais des bugs plus vite que jamais, pour les défenseurs comme pour les attaquants : l'équipe PHP a publié 15 CVE depuis juillet et trié plus d'une centaine de rapports invalides ou en double. Le rythme ne va pas ralentir. Une entreprise qui fait tourner son chiffre d'affaires sur PHP sans jamais financer ceux qui le maintiennent parie que quelqu'un d'autre paiera. Mardi soir, on a vu ce que coûte ce pari quand il ne tient plus. Mettez à jour aujourd'hui, et posez la question du budget demain.

Tags :

php securite cve docker php2shell