PHP2Shell est le nom donné à deux failles d’exécution de code à distance du cœur de PHP, CVE-2026-109618 et CVE-2026-109619, notées CVSS v4.0 9.2 et corrigées le 21 octobre 2026. Le nom vient de l’outil d’exploitation qui a fuité, pas du projet PHP. Les versions corrigées sont PHP 8.5.12, 8.4.27, 8.3.36 et 8.2.35 : toute version antérieure de ces branches est vulnérable. Si votre PHP sert du web, l’action est simple à énoncer : mettre à jour, redémarrer tous les workers, puis vérifier que personne n’est passé avant vous.
Le contexte explique l’urgence. Le 20 octobre, les détails des deux failles fuitent avant la fin de l’embargo. Le projet annonce une release pour le 21 à 14:00 UTC, en se réservant le droit de l’avancer. Il l’avance : les avis de sécurité sont publiés à 13:34 UTC. Cloudflare dit avoir vu des attaques précoces et a déployé des règles WAF sur tous ses plans, sans publier de chiffres. Depuis, Amazon Threat Intelligence a daté une première tentative d’exploitation au 17 octobre, quatre jours avant la divulgation, et parle d’une exploitation désormais large. Aucune source ne confirme à ce stade qu’une de ces tentatives ait réussi.
Publié le 21/10/2026 à 14:25 UTC, mis à jour le 22/10/2026 à 08:05 UTC : images Docker officielles et paquets Debian, Ubuntu et FrankenPHP désormais disponibles, exploitation observée par AWS et watchTowr, premiers indicateurs publiés. La situation évolue encore : revérifiez les sources primaires (php.net, avis GHSA de php-src) avant d’agir sur un point daté.
PHP2Shell, c’est quoi ? Les deux CVE et leur mécanisme
Avant le correctif, beaucoup parlaient d’un bug du parseur multipart. La source officielle dit autre chose : la cause racine est la gestion de l’état du worker quand une requête manque de mémoire ou plante. Le multipart et l’upload progress des sessions ne sont que des moyens d’y arriver.
- CVE-2026-109618 (GHSA-w8hw-w9fx-j79w, « RCE via OOM corruption ») : PHP utilise
longjmp()quand une allocation échoue. Dans certains contextes, l’état de la requête est lu avant d’être complètement initialisé. L’arrêt de la requête le traite alors comme valide. Une requête multipart construite pour épuiser la mémoire provoque cet échec avant tout code applicatif. - CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp, « RCE via stale global state ») : un état global qui pointe vers de la mémoire de requête survit à la libération de celle-ci. La requête suivante traitée par le même worker le consomme : c’est un use-after-free entre deux requêtes HTTP distinctes. Deux déclencheurs sont confirmés : les avertissements émis pendant l’initialisation INI, et le
session.upload_progress, actif par défaut et qui ne nécessite aucunsession_start(). Sur PHP 8.5, le cas le plus fort passe parfatal_error_backtraces.
Le correctif supprime le longjmp() sur manque de mémoire : le dépassement « soft » est différé au prochain point sûr de la machine virtuelle, le dépassement « hard » termine le processus par _exit(). Point important pour la suite : la fonction C exacte n’est pas nommée dans les avis, et les théories d’avant publication qui visaient main/rfc1867.c sont contredites par la source primaire.
Suis-je concerné ? mod_php, PHP-FPM, FrankenPHP
Si votre PHP sert des requêtes web, la réponse est presque toujours oui. L’avis php.net cite mod_php, PHP-FPM, php-cgi, FrankenPHP et LiteSpeed. Le risque est maximal sur Apache mod_php : le RCE complet des deux failles y est validé avec les limites par défaut, par une requête non authentifiée. Côté FPM, le RCE est validé pour la CVE-2026-109619 via l’upload progress. Pour la CVE-2026-109618, l’analyse du code dit FPM « susceptible » sans exploit complet démontré. Ne lisez pas cette nuance comme une immunité : FPM est concerné.
- Confirmé : versions corrigées, mécanismes, CVSS v4.0 9.2 (
AV:N/AC:H/AT:P/PR:N/UI:N), absence de mitigation complète. - Confirmé : des tentatives d’exploitation dans la nature. Amazon (honeypots MadPot) en observe depuis le 17/10 via
PHP_SESSION_UPLOAD_PROGRESS, watchTowr et Cloudflare le 21/10. - Non confirmé : une exploitation réussie en production. Ni AWS ni watchTowr n’en rapportent, et les deux CVE ne figuraient pas au catalogue CISA KEV (version 2026.10.21) le 22/10 au matin.
- Non confirmé : le statut du CLI et de RoadRunner, aucune déclaration officielle. PHP 8.6.0 RC4 embarque le correctif ; les RC précédentes sont vulnérables.
- Non confirmé : la borne « depuis PHP 5.4 ». Elle vient de la bannière de l’outil d’exploit, relayée par Sansec, pas du projet PHP.
Quelles versions de PHP sont corrigées ? (8.5.12, 8.4.27, 8.3.36, 8.2.35)
- PHP 8.5 : corrigé en 8.5.12, support sécurité jusqu’au 31/12/2029.
- PHP 8.4 : corrigé en 8.4.27, support sécurité jusqu’au 31/12/2028.
- PHP 8.3 : corrigé en 8.3.36, support sécurité jusqu’au 31/12/2027.
- PHP 8.2 : corrigé en 8.2.35, support sécurité jusqu’au 31/12/2026.
- PHP 8.1, 8.0 et 7.4 : aucune release officielle. L’équipe sécurité a publié des backports sources minimaux sous forme de branches git (
PHP-8.1-CVE-2026-109618-109619, idem pour 8.0 et 7.4). Leur contenu reste à examiner avant de s’y fier.
Le correctif 8.2.35 vous protège aujourd’hui, mais la branche n’a plus que dix semaines de support sécurité. Si vous y êtes encore, c’est le moment de migrer avant la fin de support de PHP 8.2. Sur 8.1 ou moins, la priorité est la migration vers 8.3 ou plus. À défaut, il reste un backport de distribution ou de fournisseur LTS, ou l’isolement de l’application, comme le recommande le CERT-FR dans son alerte CERTFR-2026-ALE-014.
Mettre à jour PHP sous Docker : rebuild, redéploiement, vérification
Être dans le périmètre ne corrige rien : il faut choisir un canal, et les canaux n’avancent pas à la même vitesse. Les images officielles corrigées php:8.5.12-fpm, 8.4.27-fpm, 8.3.36-fpm et 8.2.35-fpm, variantes -alpine et -apache comprises, sont publiées sur Docker Hub depuis le 21/10 à 22:10 UTC, soit environ huit heures après le correctif. Ne confondez pas avec les tags 8.4.27RC1-* : ce sont des release candidates, pas le correctif.
La règle à appliquer : épinglez la version corrigée plutôt qu’un tag flottant comme 8.4-fpm, qu’un cache de build peut figer sur une image vulnérable. Un garde-fou dans le Dockerfile refuse toute version inférieure, même en cas de cache ou de tag flottant. C’est le même principe qu’une image PHP multi-stage, non-root et scannée par Trivy en CI : la version réelle se vérifie au build, pas sur parole.
# Tag épinglé sur la version corrigée, jamais un tag flottant pour un correctif
FROM php:8.4.27-fpm AS base
# Garde-fou : refuse toute version < 8.4.27 (cache, tag flottant, retour arrière)
RUN php -r 'exit(version_compare(PHP_VERSION, "8.4.27", ">=") ? 0 : 1);' \
&& php-fpm -v
FROM base AS runtime
WORKDIR /var/www/app
COPY --chown=www-data:www-data . .
USER www-dataSur Debian avec le dépôt packages.sury.org d’Ondřej Surý, les paquets corrigés pour bookworm étaient en ligne dès 13:36-14:01 UTC le 21/10 (8.5.12, 8.4.27, 8.3.36, 8.2.35). Les distributions ont suivi :
- Debian 13 trixie :
php8.48.4.27-1~deb13u1 (DSA-6574-1). Debian 12 bookworm :php8.28.2.35-1~deb12u1 (DLA-4848-1). Debian forky (testing) restait vulnérable le 22/10 au matin. - Ubuntu (USN-8948) : 24.04 LTS
php8.38.3.6-0ubuntu0.24.04.13, 22.04 LTSphp8.18.1.2-1ubuntu2.28, 26.04 LTSphp8.58.5.4-0ubuntu1.5. Ce sont des backports : le numéro de version amont ne bouge pas. - FrankenPHP 1.13.2 (21/10) : images Docker en PHP 8.2.35, 8.3.36, 8.4.27 et 8.5.12, binaires statiques en 8.5.12.
- Pas encore de paquet corrigé constaté le 22/10 au matin chez Alpine (aucune entrée dans secdb) ni chez Remi, qui n’affiche que les RC1.
L’exemple Ubuntu montre le piège : un php -v qui affiche 8.3.6 sur une 24.04 à jour est corrigé, alors qu’un 8.3.35 compilé à la main ne l’est pas. Sur une distribution, la preuve est la version du paquet et son changelog, pas le numéro de PHP.
# Debian + packages.sury.org
apt-get update && apt-get install --only-upgrade -y 'php8.4-*'
systemctl restart php8.4-fpm # redémarrer TOUS les workers
php-fpm8.4 -v
apt-get changelog php8.4-fpm | grep -m3 -E 'CVE-2026-10961|8\.4\.27'- Un conteneur en cours d’exécution ne se met jamais à jour : reconstruisez l’image avec la version corrigée.
- Redéployez, puis redémarrez tous les workers PHP-FPM ou Apache : un worker resté en mémoire garde l’ancien binaire.
- Vérifiez dans le conteneur qui sert réellement le trafic :
docker compose exec php php-fpm -v.php -vdécrit le CLI et ne prouve pas la version du SAPI web.
Mitigations temporaires : ce qui aide, ce qui ne suffit pas
Si votre canal n’a pas encore livré, il faut couvrir une fenêtre. Selon l’équipe PHP, aucune mitigation de configuration n’est complète : mettre à jour PHP est la seule protection fiable. Ce qui suit réduit des chemins d’attaque connus, rien de plus, et certaines mesures ont un coût à mesurer. Les réglages officiels s’appliquent de façon non surchargeable, workers redémarrés.
; CVE-2026-109618 : non surchargeable (php_admin_value en FPM ou Apache)
; ATTENTION : memory_limit=-1 exige une limite mémoire cgroup/conteneur, sinon DoS
memory_limit = -1
post_max_size = 8M ; 32M si des uploads sont nécessaires
; CVE-2026-109619 : appliqué avant le parsing de la requête
session.upload_progress.enabled = 0
; PHP 8.5 uniquement :
fatal_error_backtraces = 0Qui s’occupe de vos mises à jour ?
Maintenance des versions PHP, framework et dépendances, à partir de 500 €/mois HT.
- Pas de
ini_set()ni de.user.ini: le réglage doit être en place avant le traitement de la requête. - Jamais de
RLIMIT_ASouRLIMIT_DATA: ils déclenchent précisément le chemin vulnérable. La limite mémoire passe par le cgroup du conteneur. pm.max_requests = 1est suggéré par l’avis de la CVE-2026-109619 si vos performances le permettent : un worker neuf par requête ne garde aucun état périmé.session.upload_progress.enabled = 0ne neutralise qu’une des deux failles (CERT-FR). Symfony ne s’en sert pas nativement : vous pouvez le couper durablement, l’avis le juge « hardly-used and hardly-tested ».session.use_strict_mode, conseillé par certains, n’a aucun effet avec des sessions Redis ou en base (Sansec). Il ne protège donc pas une stack Symfony avecRedisSessionHandler.
Au frontal, Sansec publie deux règles qui bloquent le multipart avant PHP. Elles cassent tous les uploads, y compris dans un tunnel de commande. Testez, puis retirez-les après le patch. Elles se fondent sur le Content-Type déclaré, pas sur le corps de la requête. Pour nginx, dans chaque bloc server qui transmet à PHP :
if ($http_content_type ~* "multipart/form-data") {
return 403;
}Pour Apache, dans chaque VirtualHost PHP :
RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteRule ^ - [F]Le WAF Cloudflare est présenté par Cloudflare lui-même comme une solution d’attente. Il n’a de sens que si l’origine refuse tout trafic hors des plages IP Cloudflare : un attaquant qui connaît l’IP d’origine le contourne. Le mode maintenance applicatif ne sert à rien, car PHP traite la requête avant de lire le drapeau. Et durcir le code, comme dans une checklist OWASP Symfony, ne protège pas de PHP2Shell : la faille frappe avant votre code.
Détecter une compromission après PHP2Shell
Mitiger réduit la surface, mais ne dit rien de ce qui s’est déjà passé. Premier point : la fenêtre de recherche. Amazon a observé une tentative le 17 octobre : cherchez depuis le 17/10 au minimum, pas depuis la divulgation. Second point : les tentatives observées passent par des POST multipart contenant le champ PHP_SESSION_UPLOAD_PROGRESS, un nom qu’une application Symfony n’envoie normalement jamais.
Amazon a aussi publié six adresses IP vues pendant ces tentatives. Elles appartiennent à des infrastructures d’anonymisation partagées : servez-vous-en pour enquêter dans vos logs, pas comme liste de blocage. Les logs d’accès standard ne contiennent pas le corps des requêtes : le champ ne se voit que dans les logs d’un WAF ou d’un reverse proxy qui journalise les corps.
# IP publiées par Amazon Threat Intelligence (à investiguer, pas à bloquer)
zgrep -hE '103\.216\.220\.92|31\.223\.184\.166|37\.19\.200\.5|134\.209\.159\.64|103\.114\.163\.188|95\.217\.98\.37' \
/var/log/nginx/access.log*
# Champ d’upload progress dans les logs qui contiennent les corps (WAF, proxy)
zgrep -l 'PHP_SESSION_UPLOAD_PROGRESS' /var/log/waf/*Aucun hash, nom de webshell ni règle Sigma ou YARA officielle n’est publié à cette heure. Le reste de la détection est donc comportemental, avec une limite de fond : une exploitation par corruption mémoire peut s’exécuter dans le worker sans lancer de processus. L’absence de signal ne prouve pas l’absence de compromission.
- Processus enfants de
php-fpm,httpdouapache2:sh,bash,python,perl,curl,wget,nc. Motifs/dev/tcp/etmkfifo. Reconnaissancewhoami,id,uname -a(Huntress). - Fichiers
.phprécents dans le webroot,var/, les dossiers d’upload et le répertoire temporaire. - Crontabs et comptes nouveaux,
authorized_keysmodifiés, connexions sortantes longues depuis des workers. - Crashs ou redémarrages anormaux de workers : une piste, pas une signature.
# Fichiers PHP modifiés ces 30 derniers jours, hors vendor et cache
find /var/www/app -type f -name '*.php' -mtime -30 \
-not -path '*/vendor/*' -not -path '*/var/cache/*' -ls
# Persistance
crontab -l; ls -la /etc/cron.* /var/spool/cron 2>/dev/null
# Enfants des pools php-fpm
ps -eo pid,ppid,user,args --forest | grep -A3 'php-fpm: pool'Pour une surveillance continue sur un hôte Linux sans EDR, process-watcher de Florian Roth trace un service et ses descendants en JSON, en mode observation par défaut. Il exige les droits root et un noyau 5.15 ou plus avec BTF, et se pose sur l’hôte, pas dans un conteneur. N’activez pas ses règles de kill sans test : elles tueraient aussi vos propres appels à exec() ou mail(). En cas de suspicion, isolez la machine et conservez les logs web et PHP. Faites ensuite tourner les secrets : APP_SECRET, accès base de données, contenu du .env. Reconstruire le conteneur ne suffit pas si un secret a fuité.
Checklist Symfony en production, dans l’ordre
Les failles sont dans PHP, pas dans le framework : composer update ne change rien, et aucun avis Symfony spécifique n’existe à cette heure. La méthode reste celle de la checklist appliquée lors du patch PHP 8.4.6, dans cet ordre :
- Inventaire : conteneurs de prod, staging, environnements de preview, runners CI, images FrankenPHP (à passer en 1.13.2), hébergements mutualisés oubliés.
- Patch : version corrigée épinglée, image reconstruite, redéploiement, redémarrage de tous les workers, version du SAPI vérifiée dans le conteneur.
- Mitigations partielles, tant que le canal n’a pas livré, retirées ensuite sauf
session.upload_progress.enabled = 0. - Recherche d’une compromission sur les machines exposées, depuis le 17/10 au moins, en priorité celles en mod_php.
- Rotation des secrets au moindre doute.
Contrainte de version dans composer.json : elle ne protège que l’installation, pas le runtime. Le garde-fou utile est un test sur l’image réelle, en CI, qui refuse de déployer un PHP non patché :
# .github/workflows/php-security-gate.yml : refuse un PHP non patché
name: php-security-gate
on: [push, pull_request]
jobs:
php-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t app:ci .
- name: Version PHP corrigée (CVE-2026-109618/109619)
run: |
docker run --rm --entrypoint php app:ci -r '
$min = ["8.2" => "8.2.35", "8.3" => "8.3.36", "8.4" => "8.4.27", "8.5" => "8.5.12"];
$b = PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;
if (!isset($min[$b]) || version_compare(PHP_VERSION, $min[$b], "<")) {
fwrite(STDERR, "PHP " . PHP_VERSION . " vulnérable\n"); exit(1);
}
echo "OK " . PHP_VERSION . "\n";'
docker run --rm --entrypoint php-fpm app:ci -vCe garde-fou vaut pour les images officielles et refuse volontairement 8.1 et moins. Sur Debian ou Ubuntu avec un backport, le numéro de version peut rester ancien alors que le correctif est présent : la vérification passe alors par le changelog du paquet.
Ce qu’il faut retenir
Le patch existe, c’est le seul fait qui compte. Inventoriez vos PHP, conteneurs et environnements oubliés compris. Passez en 8.5.12, 8.4.27, 8.3.36 ou 8.2.35, ou prenez le backport de votre distribution. Reconstruisez les images et redémarrez les workers. Tant que votre canal n’a pas livré, appliquez les mitigations partielles sans les prendre pour une protection. Puis cherchez les traces : patcher ferme la porte, mais ne dit pas si quelqu’un est entré.
Ce correctif rappelle une chose simple : un patch de sécurité PHP se pose en une heure quand la version tourne sur une branche supportée et que la chaîne de build est maîtrisée. Il devient un chantier quand l’application est restée en 8.1. Si votre équipe n’a pas le temps de suivre les versions de PHP, de Symfony et des dépendances, la maintenance des versions de Bear Upgrade s’en charge.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours