Skip to main content

Secure PHP Configuration: php.ini Settings for Production

Security · 19.04.2026 · 4 min read
Illustration for “Secure PHP Configuration: php.ini Settings for Production”

PHP's default settings are tuned for development, not for production. On a public server they reveal the PHP version and internal paths, and allow dangerous operations.

Finding php.ini

php --ini | grep "Loaded Configuration"
# /etc/php/8.3/fpm/php.ini

PHP-FPM and PHP CLI have different configuration files even on the same system — usually /etc/php/8.3/fpm/php.ini and /etc/php/8.3/cli/php.ini. Edits to one do not apply to the other: settings for cron jobs and console scripts need to be changed separately.

After editing any php.ini, the changes are not picked up automatically — PHP-FPM reads the file once at startup and keeps it in the worker's memory. A forgotten service restart is a common reason why a setting "seems applied" but a check still shows the old value.

sudo systemctl restart php8.3-fpm

Mandatory security settings

display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/errors.log
expose_php = Off

open_basedir = /var/www/site/:/tmp/:/var/lib/php/sessions/

disable_functions = exec,passthru,shell_exec,system,proc_open,popen,phpinfo

file_uploads = On
upload_max_filesize = 10M
max_file_uploads = 5
max_execution_time = 30
memory_limit = 256M

Anatomy of configuration mistakes

display_errors = On in production

If this setting is left at On, any uncaught PHP error gets printed straight into the HTML response and is visible to every visitor:

Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1045]
Access denied for user 'app'@'localhost' in /var/www/site/db.php:12
Stack trace:
#0 /var/www/site/index.php(5): connect()
#1 {main}
  thrown in /var/www/site/db.php on line 12

A message like this reveals the full file path on the server, the database username and the code structure — enough to plan the next step of an attack. Errors should go into error_log, not into the browser.

open_basedir set only for FPM

If the restriction is added to /etc/php/8.3/fpm/php.ini but not to cli/php.ini, console scripts and cron jobs run without it. An attack through a web form that passes data into a background CLI task bypasses a protection that was only designed for web requests.

Every virtual host on a shared server can have its own PHP-FPM pool with its own settings file — if you edit the global php.ini but a particular site runs through a pool with its own php_admin_value overriding the parameter you need, the change in the shared file will not affect that specific site.

What each setting changes

ParameterDefault valueWhat the hardened setting gives you
expose_phpOnRemoves the PHP version from the server's response header
display_errorsOnErrors go to the log instead of the HTML response shown to the visitor
open_basedirnot setPHP cannot read files outside the listed directories
disable_functionsemptyBlocks shell commands from being run from PHP code

Session security

session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
session.use_only_cookies = 1
session.use_strict_mode = 1
session.gc_maxlifetime = 1440
session.save_path = /var/lib/php/sessions

Checking the configuration

php -r "echo ini_get('expose_php');"
php -r "echo ini_get('display_errors');"

Also check the live environment, not just the CLI: request the headers of the real site and make sure the web server does not reveal the PHP version.

curl -I https://your-domain/ | grep -i x-powered-by
# Empty output means the header is not sent - correct
open_basedir restricts PHP to files inside the listed directories. This blocks Path Traversal and LFI (Local File Inclusion) attacks, where an attacker tries to read /etc/passwd.
disable_functions can be bypassed through PHP extensions. It is an extra layer of defence, not a substitute for correct code and regular updates.

When strict settings get in the way

  • An application for image or video processing needs an external binary through shell_exec (for example, calling a separate converter) — in that case remove only the specific function that is actually needed from disable_functions, rather than leaving the list empty entirely.
  • Shared hosting with several sites under different users on one PHP-FPM pool — a single open_basedir for everyone will not fit, each virtual host needs its own FPM pool with its own value.
  • Legacy code that reads files outside the site's directory (for example, from a shared templates folder) — that code needs to be fixed; widening open_basedir "just in case" brings back the exact vulnerability it is meant to prevent.

Setting up the web server in front of PHP-FPM is covered in hardening Nginx, and protecting WordPress itself on top of these settings is covered in hardening WordPress.

Was this article helpful?
← Back to Knowledge Base Ask Support