Накратко: Всеки сайт на един сървър трябва да има собствен PHP-FPM пул със собствен системен потребител и собствен сокет. Разделянето обаче действа истински едва с детайлите: права на сокета само за уеб сървъра, security.limit_extensions на .php, без публично достъпни статус страници, собствени лимити за процеси и време на изпълнение, както и отделни логове. От самия PHP2Shell защитава само актуализацията; пуловете ограничават докъде стига една атака.
Един сървър, много сайтове, общ риск
На много сървъри с годините е пораснало нещо, което никой не е планирал така. Фирменият уебсайт, магазинът, кампанийна страница от 2019 г. и тестова система работят един до друг, често под същия потребител и в същия пул. Ако забравената кампанийна страница бъде компрометирана, данните на клиентите на магазина също са изложени.
PHP-FPM, FastCGI мениджърът на процеси на PHP, решава това с пулове: групи от worker процеси със собствени настройки. Как работи основният принцип, обяснява PHP-FPM обяснен: мениджър на процеси, пулове и worker процеси. Изграждането на отделни пулове със собствен потребител, собствена временна папка и open_basedir описва ръководството open_basedir и изолация за всеки сайт. Тази статия отива една стъпка по-нататък: как да проверите съществуващите пулове и кои настройки правят разделянето устойчиво?
Важно за оценката: при PHP2Shell уязвимостите CVE-2026-109618 и CVE-2026-109619 са в интерпретатора на PHP, който всички пулове на една версия на PHP споделят. Собственият пул не предотвратява използването им. Той обаче решава дали нападателят след това достига един сайт или всички.
PHP-FPM пулове със собствен потребител: проверка на съществуващото
Преди да промените нещо, си създайте общ поглед. Следните команди само четат. Пътищата и номерата на версиите се различават според дистрибуцията:
# Всички пулове с потребител и сокет
grep -HE '^\s*(\[|user|group|listen)\s*' /etc/php/*/fpm/pool.d/*.conf
# Работещи пулове и техните потребители
ps -eo user,pid,cmd | grep '[p]hp-fpm: pool'
# Сокети и техните права
ls -l /run/php/
# Проверка на конфигурацията за грешки (не променя нищо)
sudo php-fpm8.3 -t
Обърнете внимание на три модела: няколко сайта в един и същ пул, пулове, които работят като www-data или дори като root, и сокети, в които могат да пишат всички потребители. Всеки от тях подкопава разделянето.
Ако на сървъра работят няколко версии на PHP, всяка има собствена FPM услуга със собствени директории за пулове. Проверете всички, не само версията, която сте настроили последно. Именно старите версии със забравени пулове са риск, защото често нито се поддържат, нито се рестартират след актуализации. Насоки по темата има в Няколко версии на PHP на един сървър. Запишете резултата в малка таблица: сайт, пул, потребител, сокет, версия на PHP. Този преглед е и основата за всяка по-късна промяна.
Най-важните настройки за всеки пул
Следният преглед се основава на конфигурацията на FPM в ръководството на PHP. Стойностите за процеси и времена зависят от Вашето натоварване и приложение:
| Настройка | Цел | Препоръка |
|---|---|---|
user, group |
потребител на worker процесите | собствен потребител за всеки сайт, никога root |
listen.owner, listen.group, listen.mode |
права върху сокета | само групата на уеб сървъра, режим 0660 |
listen.acl_users |
достъп чрез ACL вместо собственик | въведете само потребителя на уеб сървъра |
security.limit_extensions |
изпълними разширения | ограничете до .php (по подразбиране .php .phar) |
clear_env |
изчистване на променливите на средата | оставете стойността по подразбиране yes |
pm.max_children |
максимален брой worker процеси | ограничете така, че един сайт да не натоварва сървъра докрай |
request_terminate_timeout |
прекъсване на увиснали заявки | задайте според най-дългата легитимна заявка |
pm.status_path, ping.path |
статус и ping страница | задавайте само ако е нужно и ги защитете вътрешно |
slowlog, request_slowlog_timeout |
протокол на бавните заявки | за всеки пул в отделен файл |
Откъс за заздравен пул изглежда така:
; /etc/php/8.3/fpm/pool.d/shop.conf (откъс)
[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
Настройки като memory_limit или session.upload_progress.enabled задавате за всеки пул с php_admin_value и php_admin_flag. Според ръководството тези стойности не могат да се презапишат с ini_set(). Повече по темата в php_admin_value: заключване на настройки срещу презаписване.
Правилно разбиране на статус страниците и смяната на worker процеси
pm.status_path и ping.path са активни само ако са зададени. Статус страницата показва работещите заявки, процеси и натоварване. За мониторинг това е полезно, но при публичен достъп е излишен източник на информация за атакуващите. Препращайте тези пътища в уеб сървъра само за вътрешни адреси. Как Nginx и PHP-FPM работят сигурно заедно, описва Защита на Nginx с PHP-FPM.
pm.max_requests определя след колко заявки един worker процес се стартира наново; по подразбиране е 0, тоест никога. Стойност над нула помага срещу бавно нарастващо потребление на памет. Като защита срещу PHP2Shell той не е предвиден: php.net не го посочва сред временните мерки и подчертава, че само актуализацията защитава изцяло. След актуализацията важи и следното: всички worker процеси на всички пулове трябва да се рестартират, защото иначе старите процеси продължават да работят с уязвимия код.
Поддържайте шаблон за нови пулове и създавайте всеки нов сайт с него. Така разделянето възниква автоматично, вместо да се докарва години по-късно.
Типични грешки при прехода
Най-честата грешка е нов пул, който работи под собствен потребител, но чиито файлове продължават да принадлежат на www-data. Тогава пулът не може да пише там, където трябва, и някой задава правата твърде щедро. Изяснете предварително в кои папки трябва да може да се пише. Втората грешка са планирани задачи, които продължават да работят под стария потребител и създават файлове с неправилен собственик. Третата е обща папка за сесии за всички пулове, която отново премахва разделянето.
След всеки преход тествайте вход, качвания, кешове и планирани задачи на сайта и наблюдавайте лога за грешки на пула.
Как помага PHP SOS конкретно?
- Отчитаме всички пулове, потребители, сокети и настройки и показваме къде днес сайтовете си делят процеси или права.
- Създаваме за всеки сайт заздравен пул, с права на сокета, ограничение на разширенията, лимити на ресурсите и собствени логове.
- Защитаваме статус страниците, рестартираме контролирано всички worker процеси и тестваме всеки сайт.
- В рамките на заздравяването на сървъра документираме разделянето като шаблон за бъдещи проекти.
Често задавани въпроси
Защо всеки сайт трябва да има собствен PHP-FPM пул?
Собствен пул със собствен системен потребител разделя сайтовете на ниво процеси и права върху файловете. Ако един сайт бъде компрометиран, кодът не може лесно да чете файловете на останалите. Освен това лимитите и настройките могат да се определят отделно за всеки сайт.
Защитава ли собственият пул от PHP2Shell?
Не. Уязвимостите са в самия интерпретатор на PHP, който всички пулове използват съвместно. Защита дава само актуализацията до коригирана версия с рестартиране на всички worker процеси. Отделните пулове обаче ограничават докъде стига една успешна атака.
Какво прави security.limit_extensions?
Според ръководството на PHP настройката ограничава кои разширения на файлове PHP-FPM изпълнява като основен скрипт. По подразбиране са .php и .phar. Който ги ограничи до .php, предотвратява например качени файлове с изображения да се изпълняват като PHP, ако уеб сървърът е конфигуриран неправилно.
Опасна ли е статус страницата на PHP-FPM?
Тя издава подробности за работещите процеси и заявки. Ако е публично достъпна без защита, улеснява ориентирането на атакуващите. Според ръководството тя е активна само ако е зададен pm.status_path. Тогава уеб сървърът трябва да ограничи пътя до вътрешни адреси.