Kurz gesagt: Jede Website auf einem Server sollte einen eigenen PHP-FPM-Pool mit eigenem Systembenutzer und eigenem Socket haben. Wirksam wird die Trennung aber erst mit den Details: Socket-Rechte nur für den Webserver, security.limit_extensions auf .php, keine öffentlich erreichbaren Status-Seiten, eigene Grenzen für Prozesse und Laufzeit sowie getrennte Logs. Vor PHP2Shell selbst schützt nur das Update; Pools begrenzen, wie weit ein Angriff reicht.
Ein Server, viele Websites, ein gemeinsames Risiko
Auf vielen Servern ist über die Jahre gewachsen, was niemand so geplant hat. Die Firmenwebsite, der Shop, eine Kampagnenseite von 2019 und ein Testsystem laufen nebeneinander, oft unter demselben Benutzer und im selben Pool. Wird die vergessene Kampagnenseite kompromittiert, liegen auch die Kundendaten des Shops offen.
PHP-FPM, der FastCGI-Prozessmanager von PHP, löst das mit Pools: Gruppen von Worker-Prozessen mit eigenen Einstellungen. Wie das Grundprinzip funktioniert, erklärt PHP-FPM erklärt: Prozessmanager, Pools und Worker. Den Aufbau getrennter Pools mit eigenem Benutzer, eigenem Temp-Ordner und open_basedir beschreibt der Ratgeber open_basedir und Isolation pro Website. Dieser Beitrag geht einen Schritt weiter: Wie prüfen Sie bestehende Pools, und welche Einstellungen machen die Trennung belastbar?
Wichtig zur Einordnung: Bei PHP2Shell liegen die Lücken CVE-2026-109618 und CVE-2026-109619 im PHP-Interpreter, den alle Pools einer PHP-Version teilen. Ein eigener Pool verhindert keine Ausnutzung. Er entscheidet aber, ob ein Angreifer danach eine Website oder alle erreicht.
PHP-FPM-Pools mit eigenem User: Bestand prüfen
Bevor Sie etwas ändern, verschaffen Sie sich einen Überblick. Die folgenden Befehle lesen nur. Pfade und Versionsnummern weichen je Distribution ab:
# Alle Pools mit Benutzer und Socket
grep -HE '^\s*(\[|user|group|listen)\s*' /etc/php/*/fpm/pool.d/*.conf
# Laufende Pools und ihre Benutzer
ps -eo user,pid,cmd | grep '[p]hp-fpm: pool'
# Sockets und ihre Rechte
ls -l /run/php/
# Konfiguration auf Fehler prüfen (ändert nichts)
sudo php-fpm8.3 -t
Achten Sie auf drei Muster: mehrere Websites im selben Pool, Pools, die als www-data oder gar als root laufen, und Sockets, die für alle Benutzer beschreibbar sind. Jedes davon untergräbt die Trennung.
Laufen auf dem Server mehrere PHP-Versionen, hat jede ihren eigenen FPM-Dienst mit eigenen Pool-Verzeichnissen. Prüfen Sie alle, nicht nur die Version, die Sie zuletzt eingerichtet haben. Gerade alte Versionen mit vergessenen Pools sind ein Risiko, weil sie oft weder gepflegt noch nach Updates neu gestartet werden. Hinweise dazu stehen unter Mehrere PHP-Versionen auf einem Server. Halten Sie das Ergebnis in einer kleinen Tabelle fest: Website, Pool, Benutzer, Socket, PHP-Version. Diese Übersicht ist zugleich die Grundlage für jede spätere Änderung.
Die wichtigsten Einstellungen je Pool
Die folgende Übersicht stützt sich auf die FPM-Konfiguration im PHP-Handbuch. Werte für Prozesse und Zeiten richten sich nach Ihrer Last und Anwendung:
| Einstellung | Zweck | Empfehlung |
|---|---|---|
user, group |
Benutzer der Worker | eigener Benutzer je Website, nie root |
listen.owner, listen.group, listen.mode |
Rechte am Socket | nur Webserver-Gruppe, Modus 0660 |
listen.acl_users |
Zugriff per ACL statt Eigentümer | nur Webserver-Benutzer eintragen |
security.limit_extensions |
ausführbare Endungen | auf .php begrenzen (Standard .php .phar) |
clear_env |
Umgebungsvariablen leeren | auf Standard yes lassen |
pm.max_children |
Höchstzahl Worker | so begrenzen, dass eine Website den Server nicht auslastet |
request_terminate_timeout |
Abbruch hängender Anfragen | passend zur längsten legitimen Anfrage setzen |
pm.status_path, ping.path |
Status- und Ping-Seite | nur setzen, wenn benötigt, und intern absichern |
slowlog, request_slowlog_timeout |
Protokoll langsamer Anfragen | je Pool in eigene Datei |
Ein Auszug für einen gehärteten Pool sieht so aus:
; /etc/php/8.3/fpm/pool.d/shop.conf (Auszug)
[shop]
user = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
request_terminate_timeout = 60s
security.limit_extensions = .php
clear_env = yes
slowlog = /var/log/php/shop.slow.log
request_slowlog_timeout = 10s
php_admin_value[error_log] = /var/log/php/shop.error.log
Einstellungen wie memory_limit oder session.upload_progress.enabled setzen Sie je Pool mit php_admin_value und php_admin_flag. Laut Handbuch lassen sich diese Werte nicht per ini_set() überschreiben. Mehr dazu unter php_admin_value: Einstellungen gegen Überschreiben sperren.
Status-Seiten und Worker-Wechsel richtig einordnen
pm.status_path und ping.path sind nur aktiv, wenn sie gesetzt sind. Die Statusseite zeigt laufende Anfragen, Prozesse und Auslastung. Für Monitoring ist das hilfreich, öffentlich erreichbar aber eine unnötige Informationsquelle für Angreifer. Leiten Sie diese Pfade im Webserver nur für interne Adressen weiter. Wie Nginx und PHP-FPM sicher zusammenarbeiten, beschreibt Nginx mit PHP-FPM absichern.
pm.max_requests legt fest, nach wie vielen Anfragen ein Worker neu startet; Standard ist 0, also nie. Ein Wert größer null hilft gegen langsam wachsenden Speicherverbrauch. Als Schutz gegen PHP2Shell ist er nicht gedacht: php.net nennt ihn nicht unter den Übergangsmaßnahmen und betont, dass nur das Update vollständig schützt. Nach dem Update gilt außerdem: Alle Worker aller Pools müssen neu gestartet werden, weil alte Prozesse sonst mit dem verwundbaren Code weiterlaufen.
Pflegen Sie eine Vorlage für neue Pools und legen Sie jede neue Website damit an. So entsteht die Trennung automatisch, statt Jahre später nachgerüstet zu werden.
Typische Fehler bei der Umstellung
Der häufigste Fehler ist ein neuer Pool, der zwar unter eigenem Benutzer läuft, dessen Dateien aber weiterhin www-data gehören. Dann darf der Pool nicht schreiben, wo er muss, und jemand setzt die Rechte zu großzügig. Klären Sie vorher, welche Ordner beschreibbar sein müssen. Der zweite Fehler sind geplante Aufgaben, die weiter unter dem alten Benutzer laufen und Dateien mit falschem Eigentümer anlegen. Der dritte ist ein gemeinsamer Session-Ordner für alle Pools, der die Trennung wieder aufhebt.
Testen Sie nach jeder Umstellung Anmeldung, Uploads, Caches und geplante Aufgaben der Website und beobachten Sie das Fehlerlog des Pools.
Wie hilft PHP SOS konkret?
- Wir erfassen alle Pools, Benutzer, Sockets und Einstellungen und zeigen, wo sich Websites heute Prozesse oder Rechte teilen.
- Wir richten je Website einen gehärteten Pool ein, mit Socket-Rechten, Endungsgrenze, Ressourcengrenzen und eigenen Logs.
- Wir sichern Status-Seiten ab, starten alle Worker kontrolliert neu und testen jede Website.
- Im Rahmen der Server-Härtung dokumentieren wir die Trennung als Vorlage für künftige Projekte.
Häufige Fragen
Warum sollte jede Website einen eigenen PHP-FPM-Pool haben?
Ein eigener Pool mit eigenem Systembenutzer trennt die Websites auf Ebene der Prozesse und Dateirechte. Wird eine Website kompromittiert, kann der Code nicht ohne Weiteres die Dateien der anderen lesen. Zudem lassen sich Grenzen und Einstellungen je Website getrennt festlegen.
Schützt ein eigener Pool vor PHP2Shell?
Nein. Die Lücken liegen im PHP-Interpreter selbst, den alle Pools gemeinsam nutzen. Schutz bietet nur das Update auf eine behobene Version mit Neustart aller Worker. Getrennte Pools begrenzen aber, wie weit ein erfolgreicher Angriff reicht.
Was bewirkt security.limit_extensions?
Laut PHP-Handbuch begrenzt die Einstellung, welche Dateiendungen PHP-FPM als Hauptskript ausführt. Standard ist .php und .phar. Wer sie auf .php begrenzt, verhindert, dass etwa hochgeladene Bilddateien als PHP ausgeführt werden, falls der Webserver falsch konfiguriert ist.
Ist die Statusseite von PHP-FPM gefährlich?
Sie verrät Details über laufende Prozesse und Anfragen. Ist sie ohne Schutz öffentlich erreichbar, erleichtert sie Angreifern die Orientierung. Laut Handbuch ist sie nur aktiv, wenn pm.status_path gesetzt ist. Dann sollte der Webserver den Pfad auf interne Adressen beschränken.