Kort sagt: Varje webbplats på en server bör ha en egen PHP-FPM-pool med egen systemanvändare och egen socket. Åtskillnaden blir dock verksam först genom detaljerna: socket-rättigheter bara för webbservern, security.limit_extensions på .php, inga offentligt nåbara statussidor, egna gränser för processer och körtid samt separata loggar. Mot själva PHP2Shell skyddar bara uppdateringen; pooler begränsar hur långt ett angrepp når.
En server, många webbplatser, en gemensam risk
På många servrar har det över åren vuxit fram något som ingen planerade så. Företagets webbplats, butiken, en kampanjsida från 2019 och ett testsystem körs bredvid varandra, ofta under samma användare och i samma pool. Komprometteras den bortglömda kampanjsidan ligger även butikens kunddata öppna.
PHP-FPM, PHP:s FastCGI-processhanterare, löser det med pooler: grupper av workerprocesser med egna inställningar. Hur grundprincipen fungerar förklarar PHP-FPM förklarat: processhanterare, pooler och workers. Uppbyggnaden av separata pooler med egen användare, egen temp-mapp och open_basedir beskriver guiden open_basedir och isolering per webbplats. Det här inlägget går ett steg längre: hur kontrollerar ni befintliga pooler, och vilka inställningar gör åtskillnaden hållbar?
Viktigt för bedömningen: vid PHP2Shell finns sårbarheterna CVE-2026-109618 och CVE-2026-109619 i PHP-tolken, som alla pooler i en PHP-version delar. En egen pool förhindrar ingen exploatering. Men den avgör om en angripare därefter når en webbplats eller alla.
PHP-FPM-pooler med egen användare: kontrollera beståndet
Innan ni ändrar något bör ni skaffa er en överblick. Kommandona nedan läser bara. Sökvägar och versionsnummer skiljer sig åt mellan distributioner:
# Alla pooler med användare och socket
grep -HE '^\s*(\[|user|group|listen)\s*' /etc/php/*/fpm/pool.d/*.conf
# Pågående pooler och deras användare
ps -eo user,pid,cmd | grep '[p]hp-fpm: pool'
# Sockets och deras rättigheter
ls -l /run/php/
# Kontrollera konfigurationen efter fel (ändrar ingenting)
sudo php-fpm8.3 -t
Se upp för tre mönster: flera webbplatser i samma pool, pooler som körs som www-data eller rentav som root, och sockets som är skrivbara för alla användare. Vart och ett undergräver åtskillnaden.
Körs flera PHP-versioner på servern har varje en egen FPM-tjänst med egna poolkataloger. Kontrollera alla, inte bara den version ni senast ställde in. Just gamla versioner med bortglömda pooler är en risk, eftersom de ofta varken underhålls eller startas om efter uppdateringar. Tips om det finns under Flera PHP-versioner på en server. Dokumentera resultatet i en liten tabell: webbplats, pool, användare, socket, PHP-version. Den överblicken är samtidigt underlaget för varje senare ändring.
De viktigaste inställningarna per pool
Översikten nedan bygger på FPM-konfigurationen i PHP-manualen. Värden för processer och tider beror på er belastning och applikation:
| Inställning | Syfte | Rekommendation |
|---|---|---|
user, group |
Workerns användare | egen användare per webbplats, aldrig root |
listen.owner, listen.group, listen.mode |
Rättigheter på socketen | bara webbserverns grupp, läge 0660 |
listen.acl_users |
Åtkomst via ACL i stället för ägare | ange bara webbserveranvändaren |
security.limit_extensions |
körbara filändelser | begränsa till .php (standard .php .phar) |
clear_env |
Töm miljövariabler | låt stå på standardvärdet yes |
pm.max_children |
Högsta antal workers | begränsa så att en webbplats inte belastar servern fullt |
request_terminate_timeout |
Avbryt hängande förfrågningar | sätt för att passa den längsta legitima förfrågan |
pm.status_path, ping.path |
Status- och pingsida | sätt bara vid behov och säkra internt |
slowlog, request_slowlog_timeout |
Logg över långsamma förfrågningar | egen fil per pool |
Ett utdrag för en härdad pool ser ut så här:
; /etc/php/8.3/fpm/pool.d/shop.conf (utdrag)
[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
Inställningar som memory_limit eller session.upload_progress.enabled sätter ni per pool med php_admin_value och php_admin_flag. Enligt manualen kan dessa värden inte skrivas över med ini_set(). Mer om det under php_admin_value: spärra inställningar mot överskrivning.
Statussidor och workerbyte i rätt perspektiv
pm.status_path och ping.path är bara aktiva när de är satta. Statussidan visar pågående förfrågningar, processer och belastning. För övervakning är det användbart, men offentligt nåbart är det en onödig informationskälla för angripare. Vidarebefordra dessa sökvägar i webbservern bara för interna adresser. Hur Nginx och PHP-FPM samarbetar säkert beskriver Säkra Nginx med PHP-FPM.
pm.max_requests anger efter hur många förfrågningar en worker startas om; standard är 0, alltså aldrig. Ett värde större än noll hjälper mot långsamt växande minnesförbrukning. Som skydd mot PHP2Shell är det inte tänkt: php.net nämner det inte bland övergångsåtgärderna och betonar att bara uppdateringen skyddar fullständigt. Efter uppdateringen gäller dessutom: alla workers i alla pooler måste startas om, eftersom gamla processer annars fortsätter att köra med den sårbara koden.
Underhåll en mall för nya pooler och skapa varje ny webbplats med den. Då uppstår åtskillnaden automatiskt i stället för att kompletteras år senare.
Typiska fel vid omställningen
Det vanligaste felet är en ny pool som visserligen körs under egen användare men vars filer fortfarande ägs av www-data. Då får poolen inte skriva där den måste, och någon sätter rättigheterna för generöst. Klargör i förväg vilka mappar som måste vara skrivbara. Det andra felet är schemalagda uppgifter som fortsätter att köras under den gamla användaren och skapar filer med fel ägare. Det tredje är en gemensam sessionsmapp för alla pooler, som upphäver åtskillnaden igen.
Testa efter varje omställning webbplatsens inloggning, uppladdningar, cacher och schemalagda uppgifter och följ poolens fellogg.
Hur hjälper PHP SOS konkret?
- Vi kartlägger alla pooler, användare, sockets och inställningar och visar var webbplatser i dag delar processer eller rättigheter.
- Vi ställer in en härdad pool per webbplats, med socket-rättigheter, ändelsegräns, resursgränser och egna loggar.
- Vi säkrar statussidor, startar om alla workers kontrollerat och testar varje webbplats.
- Inom ramen för serverhärdningen dokumenterar vi åtskillnaden som mall för framtida projekt.
Vanliga frågor
Varför bör varje webbplats ha en egen PHP-FPM-pool?
En egen pool med egen systemanvändare skiljer webbplatserna åt på process- och filrättighetsnivå. Komprometteras en webbplats kan koden inte utan vidare läsa de andras filer. Dessutom går det att fastställa gränser och inställningar separat per webbplats.
Skyddar en egen pool mot PHP2Shell?
Nej. Sårbarheterna finns i själva PHP-tolken, som alla pooler delar. Skydd ger bara uppdateringen till en rättad version med omstart av alla workers. Separata pooler begränsar dock hur långt ett lyckat angrepp når.
Vad gör security.limit_extensions?
Enligt PHP-manualen begränsar inställningen vilka filändelser PHP-FPM kör som huvudskript. Standard är .php och .phar. Den som begränsar den till .php hindrar att till exempel uppladdade bildfiler körs som PHP om webbservern är felkonfigurerad.
Är PHP-FPM:s statussida farlig?
Den avslöjar detaljer om pågående processer och förfrågningar. Är den offentligt nåbar utan skydd underlättar den orienteringen för angripare. Enligt manualen är den bara aktiv när pm.status_path är satt. Då bör webbservern begränsa sökvägen till interna adresser.