Kort gezegd: Elke website op een server hoort een eigen PHP-FPM-pool te hebben, met een eigen systeemgebruiker en een eigen socket. De scheiding werkt echter pas met de details: socketrechten alleen voor de webserver, security.limit_extensions op .php, geen openbaar bereikbare statuspagina's, eigen limieten voor processen en looptijd en gescheiden logs. Tegen PHP2Shell zelf beschermt alleen de update; pools beperken hoe ver een aanval reikt.
Eén server, veel websites, één gedeeld risico
Op veel servers is er door de jaren heen iets gegroeid wat niemand zo heeft gepland. De bedrijfswebsite, de webwinkel, een campagnesite uit 2019 en een testsysteem draaien naast elkaar, vaak onder dezelfde gebruiker en in dezelfde pool. Wordt de vergeten campagnesite gecompromitteerd, dan liggen ook de klantgegevens van de webwinkel open.
PHP-FPM, de FastCGI-procesmanager van PHP, lost dat op met pools: groepen workerprocessen met eigen instellingen. Hoe het basisprincipe werkt, legt PHP-FPM uitgelegd: procesmanager, pools en workers uit. De opbouw van gescheiden pools met een eigen gebruiker, een eigen tijdelijke map en open_basedir beschrijft de gids open_basedir en isolatie per website. Dit artikel gaat een stap verder: hoe controleert u bestaande pools en welke instellingen maken de scheiding stevig?
Belangrijk voor de duiding: bij PHP2Shell zitten de kwetsbaarheden CVE-2026-109618 en CVE-2026-109619 in de PHP-interpreter, die alle pools van één PHP-versie delen. Een eigen pool voorkomt geen misbruik. Hij bepaalt wel of een aanvaller daarna één website bereikt of alle.
PHP-FPM-pools met eigen gebruiker: bestaande situatie controleren
Voordat u iets wijzigt, verschaft u zich een overzicht. De volgende opdrachten lezen alleen. Paden en versienummers verschillen per distributie:
# Alle pools met gebruiker en socket
grep -HE '^\s*(\[|user|group|listen)\s*' /etc/php/*/fpm/pool.d/*.conf
# Draaiende pools en hun gebruikers
ps -eo user,pid,cmd | grep '[p]hp-fpm: pool'
# Sockets en hun rechten
ls -l /run/php/
# Configuratie op fouten controleren (wijzigt niets)
sudo php-fpm8.3 -t
Let op drie patronen: meerdere websites in dezelfde pool, pools die als www-data of zelfs als root draaien, en sockets die voor alle gebruikers beschrijfbaar zijn. Elk daarvan ondermijnt de scheiding.
Draaien er meerdere PHP-versies op de server, dan heeft elke versie een eigen FPM-service met eigen poolmappen. Controleer ze allemaal, niet alleen de versie die u het laatst hebt ingericht. Juist oude versies met vergeten pools zijn een risico, omdat ze vaak noch worden onderhouden noch na updates worden herstart. Aanwijzingen daarover staan onder Meerdere PHP-versies op één server. Leg het resultaat vast in een kleine tabel: website, pool, gebruiker, socket, PHP-versie. Dit overzicht is tevens de basis voor elke latere wijziging.
De belangrijkste instellingen per pool
Het volgende overzicht steunt op de FPM-configuratie in de PHP-handleiding. Waarden voor processen en tijden hangen af van uw belasting en applicatie:
| Instelling | Doel | Aanbeveling |
|---|---|---|
user, group |
Gebruiker van de workers | eigen gebruiker per website, nooit root |
listen.owner, listen.group, listen.mode |
Rechten op de socket | alleen webservergroep, modus 0660 |
listen.acl_users |
Toegang via ACL in plaats van eigenaar | alleen webservergebruiker invullen |
security.limit_extensions |
uitvoerbare extensies | beperken tot .php (standaard .php .phar) |
clear_env |
Omgevingsvariabelen leegmaken | op standaard yes laten |
pm.max_children |
Maximumaantal workers | zo beperken dat één website de server niet vol belast |
request_terminate_timeout |
Afbreken van vastgelopen verzoeken | afstemmen op het langste legitieme verzoek |
pm.status_path, ping.path |
Status- en pingpagina | alleen instellen indien nodig en intern beveiligen |
slowlog, request_slowlog_timeout |
Logboek van trage verzoeken | per pool in een eigen bestand |
Een uittreksel voor een verharde pool ziet er zo uit:
; /etc/php/8.3/fpm/pool.d/shop.conf (uittreksel)
[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
Instellingen als memory_limit of session.upload_progress.enabled stelt u per pool in met php_admin_value en php_admin_flag. Volgens de handleiding zijn deze waarden niet met ini_set() te overschrijven. Meer daarover onder php_admin_value: instellingen tegen overschrijven vergrendelen.
Statuspagina's en workerwissel op de juiste plaats zetten
pm.status_path en ping.path zijn alleen actief als ze zijn ingesteld. De statuspagina toont lopende verzoeken, processen en belasting. Voor monitoring is dat nuttig, maar openbaar bereikbaar is het een onnodige informatiebron voor aanvallers. Stuur deze paden in de webserver alleen door voor interne adressen. Hoe Nginx en PHP-FPM veilig samenwerken, beschrijft Nginx met PHP-FPM beveiligen.
pm.max_requests bepaalt na hoeveel verzoeken een worker opnieuw start; standaard is 0, dus nooit. Een waarde groter dan nul helpt tegen langzaam groeiend geheugengebruik. Als bescherming tegen PHP2Shell is hij niet bedoeld: php.net noemt hem niet onder de overbruggingsmaatregelen en benadrukt dat alleen de update volledig beschermt. Na de update geldt bovendien: alle workers van alle pools moeten worden herstart, omdat oude processen anders met de kwetsbare code doordraaien.
Onderhoud een sjabloon voor nieuwe pools en leg elke nieuwe website daarmee aan. Zo ontstaat de scheiding vanzelf, in plaats van jaren later achteraf te worden ingebouwd.
Typische fouten bij de omschakeling
De meest voorkomende fout is een nieuwe pool die weliswaar onder een eigen gebruiker draait, maar waarvan de bestanden nog steeds van www-data zijn. Dan mag de pool niet schrijven waar dat moet en stelt iemand de rechten te ruimhartig in. Zoek vooraf uit welke mappen beschrijfbaar moeten zijn. De tweede fout zijn geplande taken die onder de oude gebruiker blijven draaien en bestanden met een verkeerde eigenaar aanmaken. De derde is een gemeenschappelijke sessiemap voor alle pools, die de scheiding weer opheft.
Test na elke omschakeling het inloggen, uploads, caches en geplande taken van de website en houd het foutenlog van de pool in de gaten.
Hoe helpt PHP SOS concreet?
- Wij inventariseren alle pools, gebruikers, sockets en instellingen en tonen waar websites vandaag processen of rechten delen.
- Wij richten per website een verharde pool in, met socketrechten, extensiebeperking, resourcelimieten en eigen logs.
- Wij beveiligen statuspagina's, herstarten alle workers gecontroleerd en testen elke website.
- In het kader van de serverhardening documenteren wij de scheiding als sjabloon voor toekomstige projecten.
Veelgestelde vragen
Waarom zou elke website een eigen PHP-FPM-pool moeten hebben?
Een eigen pool met een eigen systeemgebruiker scheidt de websites op het niveau van processen en bestandsrechten. Wordt één website gecompromitteerd, dan kan de code niet zomaar de bestanden van de andere lezen. Bovendien kunnen limieten en instellingen per website afzonderlijk worden vastgelegd.
Beschermt een eigen pool tegen PHP2Shell?
Nee. De kwetsbaarheden zitten in de PHP-interpreter zelf, die alle pools gemeenschappelijk gebruiken. Bescherming biedt alleen de update naar een verholpen versie met herstart van alle workers. Gescheiden pools beperken wel hoe ver een geslaagde aanval reikt.
Wat doet security.limit_extensions?
Volgens de PHP-handleiding beperkt de instelling welke bestandsextensies PHP-FPM als hoofdscript uitvoert. Standaard zijn dat .php en .phar. Wie ze beperkt tot .php, voorkomt dat bijvoorbeeld geüploade afbeeldingsbestanden als PHP worden uitgevoerd, mocht de webserver verkeerd zijn geconfigureerd.
Is de statuspagina van PHP-FPM gevaarlijk?
Ze verraadt details over lopende processen en verzoeken. Is ze zonder bescherming openbaar bereikbaar, dan helpt ze aanvallers bij de oriëntatie. Volgens de handleiding is ze alleen actief als pm.status_path is ingesteld. Dan moet de webserver het pad beperken tot interne adressen.