Un correctif comme celui de PHP2Shell bouche une faille précise. Il ne change rien à ce qui se passe le jour où la prochaine sort — et il y en aura une prochaine. La vraie question opérationnelle n'est pas « comment éviter toute RCE » (personne n'y arrive), c'est « qu'est-ce qu'un attaquant peut atteindre une fois dedans ». Ce tutoriel configure un pool PHP-FPM isolé pour qu'une exécution de code dans une appli ne donne pas accès au reste du serveur — la même logique de défense en profondeur qu'OpenAI décrit pour ses environnements d'entraînement, appliquée à un serveur web PHP ordinaire. C'est particulièrement pertinent pour un endpoint qui appelle un LLM ou expose un agent : une clé d'API ou des identifiants de service y traînent presque toujours en mémoire ou en variable d'environnement.

Étape 1 — un pool par application

Par défaut, beaucoup d'installations PHP-FPM font tourner toutes les applications d'un serveur dans le même pool www, sous le même utilisateur système. Une RCE dans l'une donne un accès en lecture/écriture à toutes les autres. Première étape : un pool dédié par application, dans son propre fichier sous /etc/php/8.4/fpm/pool.d/.

; /etc/php/8.4/fpm/pool.d/monapp.conf
[monapp]
user = monapp-fpm
group = monapp-fpm
listen = /run/php/monapp.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Étape 2 — un utilisateur système dédié, sans shell

Le user/group du pool doit être propre à cette appli, pas un compte partagé ni www-data. Un attaquant qui obtient l'exécution de code hérite des droits de ce compte : autant qu'ils ne mènent nulle part.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin monapp-fpm

Étape 3 — limiter les ressources du pool

Un pool sans limites explicites peut saturer la mémoire ou les descripteurs de fichiers du serveur entier — utile aussi contre une boucle qui part en vrille, pas seulement contre une attaque.

pm = dynamic
pm.max_children = 8
pm.max_requests = 500          ; recycle le worker périodiquement
request_terminate_timeout = 30s
rlimit_files = 1024
rlimit_core = 0                ; pas de core dump (peut contenir des secrets)

Étape 4 — restreindre le système de fichiers

open_basedir confine les fonctions fichier de PHP à une liste de chemins — un processus compromis ne peut pas lire /etc/passwd ni le code d'une autre appli sur le même serveur, même s'il peut exécuter du PHP arbitraire. disable_functions retire l'accès direct au shell.

php_admin_value[open_basedir] = /var/www/monapp:/tmp
php_admin_value[disable_functions] = exec,shell_exec,proc_open,popen,system,passthru

Étape 5 — filtrer l'egress réseau du pool

Une RCE qui ne peut pas téléphoner dehors perd l'essentiel de son intérêt pour l'attaquant : pas d'exfiltration, pas de canal de command and control. Comme le pool tourne sous un utilisateur système dédié, une règle iptables peut cibler ce seul egress sans toucher au reste du trafic du serveur :

# Autoriser uniquement l'API LLM utilisée par monapp, tout bloquer d'autre en sortie
iptables -A OUTPUT -m owner --uid-owner monapp-fpm -d api.anthropic.com -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner monapp-fpm -j REJECT

Vérifier que l'isolation tient

# Le worker tourne bien sous le compte dédié
ps -eo user,cmd | grep php-fpm

# open_basedir bloque la lecture hors périmètre
php -d open_basedir=/var/www/monapp -r "readfile('/etc/passwd');"
# -> Warning: open_basedir restriction in effect

# L'egress est bien coupé pour autre chose que l'API autorisée
sudo -u monapp-fpm curl -m 3 https://example.com
# -> curl: (7) Failed to connect (REJECT)
Ce qu'un attaquant atteintPool partagéPool isolé
Fichiers des autres applisouinon (open_basedir)
Shell systèmeouinon (disable_functions)
Sortie réseau arbitraireouinon (egress filtré)
Ressources du serveur entierouilimitées au pool
Aucune de ces barrières n'empêche la RCE elle-même — elles réduisent ce qu'elle rapporte une fois obtenue.

Générateur de pool

Un petit outil pour générer le squelette de configuration à partir de quelques paramètres :


Règle

Un pool par application, un utilisateur système par pool, open_basedir et disable_functions systématiques, egress filtré sur l'utilisateur du pool. Rien de ça n'empêche une RCE — seul le patch le fait. Mais le jour où une faille sort avant le correctif, c'est la différence entre un incident contenu à une appli et un serveur entier compromis.

FAQ

Est-ce que ça remplace les mises à jour de sécurité ?

Non. L'isolation de pool limite les dégâts d'une compromission, elle ne l'empêche pas. Les deux sont nécessaires : patcher reste la priorité, l'isolation est le filet en dessous.

Pourquoi un utilisateur système dédié plutôt que www-data ?

Parce que www-data est souvent partagé entre plusieurs applications sur le même serveur. Si une RCE obtient les droits de www-data, elle obtient potentiellement accès à tout ce qui tourne sous ce compte, pas seulement l'appli visée.

open_basedir suffit-il à bloquer une sortie réseau ?

Non, open_basedir ne restreint que les fonctions fichier de PHP. Bloquer l'egress réseau est une mesure séparée, au niveau du pare-feu, ciblée sur l'utilisateur système du pool.