Cybersecurity

PHP Hardening: php.ini and PHP-FPM Pool Settings That Matter (and Their Limits)

Practical php.ini and PHP-FPM settings for production: hide versions, log instead of display, safer session cookies, sane limits, and per-site pools.

Published
Reading time
8 min read

PHP's defaults favour compatibility, not a server facing the internet. A short list of php.ini and PHP-FPM pool settings reduces what your server leaks and limits what a single compromised script can reach. None of them replaces keeping PHP patched or fixing vulnerable application code, and two popular ones, disable_functions and open_basedir, are weaker than many guides suggest. Every directive below is described on php.net; paths are for Debian and Ubuntu packages, using PHP 8.4 as the example.

Where to put the settings

Each SAPI reads its own file: the website uses /etc/php/8.4/fpm/php.ini, not the CLI file. After php.ini, PHP reads every .ini file in the scan directory (/etc/php/8.4/fpm/conf.d/) in alphabetical order, so a file such as 99-hardening.ini keeps your changes separate from the packaged file and survives package upgrades cleanly. Settings that should differ per site go in the pool file with php_admin_value or php_admin_flag; values set that way cannot be overridden by ini_set() in application code.

Check what you already have before changing anything. Debian builds its web php.ini from upstream's php.ini-production, which already sets display_errors = Off and log_errors = On, and Debian additionally switches expose_php to Off and adds the pcntl_* functions to disable_functions:

grep -E '^(expose_php|display_errors|log_errors|disable_functions|allow_url_fopen)' \
  /etc/php/8.4/fpm/php.ini /etc/php/8.4/fpm/conf.d/*.ini

Stop leaking details

; /etc/php/8.4/fpm/conf.d/99-hardening.ini
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED

expose_php controls the X-Powered-By: PHP/x.y.z header; it can only be set in php.ini. The manual calls hiding PHP a minor measure (security by obscurity), but there is no reason to advertise your exact version. display_errors must be off in production: error messages reveal file paths, queries and sometimes credentials. With log_errors on, errors go to the file named by error_log; the file must be writable by the pool's user, so set it per pool (shown below).

Safer session cookies

These apply to PHP's native session module (session_start()). Frameworks that manage their own cookies have equivalent settings in their configuration.

session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax

use_strict_mode is off by default, yet the manual calls enabling it mandatory for general session security: it rejects session IDs the server did not create, which blocks session fixation by adoption. cookie_httponly keeps the cookie away from JavaScript; cookie_secure sends it only over HTTPS, so enable it only when the whole site is on HTTPS; cookie_samesite = Lax stops the cookie being sent with cross-site POST requests. use_only_cookies is already on by default, and disabling it is deprecated as of PHP 8.4.

Remote includes and uploads

allow_url_include = Off
file_uploads = On
upload_max_filesize = 16M
post_max_size = 20M
max_file_uploads = 20
memory_limit = 256M
max_execution_time = 30

allow_url_include (off by default, and deprecated since PHP 7.4) must stay off: it would let include load code from a URL. allow_url_fopen is on by default and lets functions such as file_get_contents() open URLs; turn it off only after testing, because some applications rely on it. Size the upload limits to what the application really needs: post_max_size must be larger than upload_max_filesize, and if a request exceeds post_max_size, PHP leaves $_POST and $_FILES empty. Nginx enforces its own limit first (client_max_body_size, default 1m), so raise both together. Note that from PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, memory_limit is a soft limit, with a separate hard limit controlled by memory_hard_limit_percent.

disable_functions and open_basedir: guard rails, not walls

disable_functions takes a comma-separated list of internal functions and can only be set in php.ini. Since PHP 8.0 a disabled function is removed entirely. The manual warns that it can be circumvented and should not be considered a sufficient security measure for shared hosting. If you use it, copy Debian's existing list first (a later file replaces the earlier value), add functions such as exec,passthru,shell_exec,system,proc_open,popen only after confirming your application never calls them, and remember the CLI has its own php.ini. In a pool file, php_admin_value[disable_functions] appends to the php.ini list rather than replacing it. The related disable_classes directive was removed in PHP 8.5.

open_basedir limits file access to listed directory trees, separated by colons on Linux, and resolves symbolic links. A script can tighten it at runtime but not loosen it. PHP's own security classification policy lists issues that require an open_basedir bypass among those it does not treat as security issues, so use it to catch mistakes, not as the boundary between customers. Real separation comes from running each site as its own Unix user with strict file permissions, or in separate containers or virtual machines.

A hardened pool per site

; /etc/php/8.4/fpm/pool.d/shop.conf
[shop]
user = shop
group = shop
listen = /run/php/php8.4-fpm-shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
security.limit_extensions = .php
php_admin_value[error_log] = /var/www/shop/logs/php-error.log
php_admin_flag[log_errors] = on
php_admin_flag[display_errors] = off
php_admin_value[open_basedir] = /var/www/shop/:/var/lib/php/sessions/:/tmp/
  • One user per site. The user and group lines decide what a compromised script can read and write. Keep the logs directory outside the web root and writable by that user.
  • Unix socket, not an open port. The FPM manual warns that an exposed FastCGI endpoint allows arbitrary code execution. Prefer a Unix socket with listen.owner, listen.group and listen.mode. If you must use TCP, set listen.allowed_clients, which is unset by default and then accepts any address; in containers, do not publish the FPM port on the host.
  • security.limit_extensions = .php narrows the default (.php .phar) so a web-server mistake cannot make FPM execute other file types.
  • clear_env defaults to yes; leave it, so arbitrary environment variables do not reach workers.
  • Sessions and open_basedir. Debian stores sessions in /var/lib/php/sessions; include it (and the upload temporary directory) when you set open_basedir, then test logins and uploads.

Apply and verify

sudo php-fpm8.4 -t
sudo systemctl restart php8.4-fpm
curl -sI https://example.com/ | grep -i x-powered-by

No X-Powered-By header from PHP means expose_php is off (some applications set the header themselves). To confirm effective values, use a temporary script with a random name that prints ini_get('display_errors'), ini_get('session.use_strict_mode') and similar, open it once, and delete it. Then click through login, uploads and any feature that sends email or calls external APIs.

What you should do

  1. Put production settings in a conf.d file for the FPM SAPI and keep per-site values in pool files with php_admin_value.
  2. Turn off display_errors and expose_php, and log errors to a per-site file outside the web root.
  3. Enable session.use_strict_mode, HttpOnly, Secure (on HTTPS sites) and SameSite cookies.
  4. Run each site in its own pool and Unix user on a Unix socket, with security.limit_extensions = .php.
  5. Treat disable_functions and open_basedir as extra layers only, and keep PHP itself patched; the October 2026 PHP security releases are a reminder that no configuration replaces updates.

Sources

SKYLINE Engineering

@skyline

The engineering team at SKYLINE Industrial Solutions. We publish field-tested guides drawn from real KSA and GCC deployments.

See author profile
SKYLINE engineering services

Need this implemented for you?

Reading is free — building it right takes a team. SKYLINE engineers ship Cybersecurity for Aramco vendors, banks, hospitals and government agencies across Saudi Arabia. Talk to us before you start.

Aramco Approved Contractor ISO 9001 · ISO 27001 SAMA CSF aligned NCA ECC ready 247+ KSA clients

Comments

0 total · 0 threads
Be the first to leave a comment.