Web server
PHP should not run under the same uid/gid (user/group) as the web server (“apache”, “www-data”, “nginx”, etc.). This problem is common when running Apache with mod_php, or in “lazy” php-fpm setups where one pool is shared by all virtual hosts. In setups like this, a compromise of one vulnerable site can easily spread to other sites whose file permissions allow the vulnerable site write access. To control the uid/gid that PHP runs under, we’ll use PHP-FPM. PHP-FPM is fully supported, and we have good documentation covering usage and implementation here.
PHP-FPM
Individual Pool configuration
When installing PHP-FPM, the basic pool configuration should contain the following information (pool_name.conf):
User & Group
When we are creating a pool, as an additional step we must add a local user with its respective group. This will allow us to isolate the application from other users on the server.
Basic pool configuration
; Mandatory name, can’t be a variable.
[pool_name];
You could use $pool to replace pool_name on this config file.
user = username
group = usergroup
; How we communicate with the web server:
listen = /run/php-fpm/pool_name.sock
; This might vary depending on the distro (Ubuntu or RHEL based, on Ubuntu the user and group is www-data).
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
listen.allowed_clients = 127.0.0.1
; We are setting ondemand to avoid tuning on each corner.
; If the application grows it will just spawn more children’s or in any case,
we just increase max_children and nothing else.
pm = ondemand
pm.max_children = 50
pm.max_requests = 500
pm.process_idle_timeout = 10s
catch_workers_output = yes
access.log = /var/log/php-fpm/pool_name-access.log ;
Disable this variable under production.
slowlog = /var/log/php-fpm/pool_name-slow.log ;
Disable this variable under production.
request_slowlog_timeout = 15s
request_terminate_timeout = 20s
php_flag[display_errors] = on ; Disable this variable under production.
php_admin_value[error_log] = /var/log/php-fpm/pool_name-error.
logphp_admin_flag[log_errors] = on ; Disable this variable under production. php_admin_value[memory_limit] = 512M
php_admin_value[open_basedir] = /var/www/vhosts/mywebsite.com/html:/var/www/vhosts/mywebsite.com/tmp
php_admin_value[allow_url_fopen] = Off
php_admin_value[disable_functions] = dl,enable_dl,exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,s
how_source,eval
php_admin_value[sys_temp-dir] =/var/www/vhosts/mywebsite.com/tmp
php_admin_value[upload_tmp_dir] = /var/www/vhosts/mywebsite.com/tmp
php_value[session.save_handler] = files
php_value[session.save_path] = /var/www/vhosts/mywebsite.com/tmp
env[HOSTNAME] = $HOSTNAME
env[TMP] = /var/www/vhosts/mywebsite.com/tmp
env[TMPDIR] = /var/www/vhosts/mywebsite.com/tmp
env[TEMP] = /var/www/vhosts/mywebsite.com/tmp
Note
The eval() language construct is very dangerous because it allows execution of arbitrary PHP code. Its use thus is discouraged. If you have carefully verified that there is no other option than to use this construct, pay special attention not to pass any user provided data into it without properly validating it beforehand: https://www.php.net/manual/en/function.eval.php
Real_path cache & open_basedir
open_basedir & real_path
Note that open_basedir increases security but also disables realpath cache, so it can have a performance hit when many files are accessed: https://www.php.net/manual/en/ini.core.php#ini.open-basedir
General configuration
On the file php-fpm.conf modify the value security.limit_extensions and leave only .php:
php-fpm.conf
security.limit_extensions = .php
Addendum
session.save_path can be moved to Redis or Memcache and avoid i/o depending on the customer application. As you can see, I delegate the configuration of disable_functions to the PHP-FPM pool, so if I want to use composer I won’t have problems with the latter.
PHP configuration
One of the reasons why we see so many remote “hacks” or “injections” is because we let remote code be included or evaluated locally, this happens after an insecure variable allows remote reading of a script. To disable this, we will change the following values in the php.ini file:
php.ini
allow_url_fopen = Off ; This last value is “deprecated” since PHP 7.4.0 allow_url_include = Off
PHP-FPM Pool Status
NOTE: BE CAREFUL when enabling this feature, as it may be a security breach.
The goal of enabling php-fpm pool status is to get a real status of how the pool is performing. Something like this:
curl https://www.example.com/status –resolve www.example.com:443:127.0.0.1
pool: www.example.com
process manager: dynamic
start time: 23/Oct/2010:19:38:41 -0500
start since: 149240
accepted conn: 173222
listen queue: 0
max listen queue: 0
listen queue len: 0
idle processes: 449
active processes: 1
total processes: 450
max active processes: 341
max children reached: 0slow requests: 0
To get it to work, do the following:
1. The pool configuration file has to have the following directive, either, uncomment it or add it, if not already there
2. # grep pm.status /etc/php-fpm.d/www.example.com.conf
3. pm.status_path = /status
4. # php-fpm -t
5. # systemctl reload php-fpm
6. Edit the vhost configuration file and insert the directives withing the VirtualHost block
7. For nginx:
8. location ~ ^/(status|ping)$ {
9. allow 127.0.0.1;
10. #allow 1.2.3.4;
11. deny all;
12. fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
13. fastcgi_index index.php;
14. include fastcgi_params;
15. #fastcgi_pass 127.0.0.1:9000;
16. fastcgi_pass unix:/run/php-fpm/www.sock;
17. }
18.# nginx -t
19.# systemctl reload nginx
20.For Apache:
21. <LocationMatch “/status”>
22. Order Allow,Deny
23. Allow from 127.0.0.1
24. ProxyPass unix:/var/run/php-fpm.sock|fcgi://localhost/status
25. </LocationMatch>
26.# httpd -t
27. # systemctl reload httpd
28. Finally, check it with curl
# curl https://www.example.com/status –resolve www.example.com:443:127.0.0.1
Final notes
When using this method, all permissions were 644 and 755, therefore we will not have to grant insecure permissions for files or folders.
Related sites
Related articles