
How to secure PHP settings on a web server?
To secure PHP settings on a web server, you edit the php.ini configuration (or a per-site pool file) to hide version information, stop showing errors to visitors, disable functions your applications don't need, restrict which directories PHP can read, and tighten session cookies. You then run a supported PHP 8.x version, reload PHP-FPM or your web server, and confirm the changes took effect. None of these steps replace secure application code, but together they limit how much damage a single bug or malicious upload can do.
PHP ships with defaults that favour compatibility over security, and many hosting setups never change them. This guide walks you through finding the right configuration file, the directives that matter most, how to apply different settings per site, and how to test that everything still works. The examples assume Ubuntu or Debian with PHP-FPM, but the directives themselves are the same on any platform.
Why PHP Configuration Matters for Security
PHP runs your application code, so its configuration decides what that code is allowed to do. If an attacker finds a file upload flaw or a vulnerable plugin, the PHP settings determine whether they can run shell commands, read files outside your website, include remote code, or see detailed error messages that reveal your file paths and database structure.
Good PHP hardening helps you:
- Reduce information leakage: Visitors and scanners shouldn't learn your exact PHP version, file paths, or stack traces.
- Limit the blast radius of a compromise: Disabling dangerous functions and restricting file access makes it much harder to turn a small bug into full server control.
- Protect user sessions: Secure cookie flags make session hijacking harder.
- Control resource abuse: Sensible limits on memory, execution time, and upload size reduce the impact of abusive requests.
Think of it as defence in depth. Your application should be secure on its own, and PHP settings are the safety net underneath it.
Step 1: Run a Supported PHP Version
Before tuning any directives, make sure you're running a PHP version that still receives security fixes. Each PHP branch gets active support for roughly two years, followed by about two years of security-only fixes, after which it reaches end of life. Running an end-of-life version means newly discovered vulnerabilities in PHP itself will never be patched.
Check your version from the command line:
php -v
Keep in mind that the command-line PHP and the version your web server uses can differ. To check what your site actually runs, look at your hosting panel, or temporarily create a file with phpinfo() in it, load it once, and delete it immediately afterwards. Better still, check the PHP-FPM service name:
systemctl list-units --type=service | grep php
Check the official PHP "Supported Versions" page to confirm your branch is still maintained, and plan upgrades well before the end-of-life date. WordPress 6.x runs well on PHP 8.1 and newer.
Step 2: Find the Right php.ini File
PHP often has several configuration files, one for each "SAPI" (the way PHP is run). On Ubuntu and Debian you'll typically see:
/etc/php/8.3/fpm/php.inifor PHP-FPM (used by Nginx and most modern Apache setups)/etc/php/8.3/apache2/php.inifor Apache'smod_php/etc/php/8.3/cli/php.inifor command-line scripts, cron jobs, and WP-CLI
Replace 8.3 with your version. On RHEL, Rocky Linux, or AlmaLinux, the main file is usually /etc/php.ini with extra files in /etc/php.d/.
To see which file the command-line PHP loads:
php --ini
Rather than editing the main php.ini directly, it's cleaner to create your own override file. On Debian-based systems, drop a file into the conf.d directory so package upgrades don't overwrite your changes:
sudo nano /etc/php/8.3/fpm/conf.d/99-hardening.ini
Files in conf.d load in alphabetical order, so the 99- prefix ensures your settings load last and win. Before you change anything, back up the current configuration:
sudo cp -a /etc/php/8.3 /root/php-8.3-backup-$(date +%F)
Step 3: Hide PHP Version Information
By default, PHP adds an X-Powered-By: PHP/8.x.y header to every response. That tells automated scanners exactly which version you run, which makes it easier to match your server to known vulnerabilities.
; Don't advertise the PHP version in HTTP headers
expose_php = Off
Hiding the version isn't real protection on its own, since a determined attacker can often fingerprint software in other ways, but there's no reason to hand out the information for free. Pair this with hiding your web server version (server_tokens off; in Nginx or ServerTokens Prod in Apache).
Step 4: Stop Displaying Errors to Visitors
Error messages are extremely useful during development and extremely useful to attackers in production. A single warning can reveal full file paths, database table names, or snippets of code. On a live server, errors should be logged, not shown.
; Never show errors in the browser on production
display_errors = Off
display_startup_errors = Off
; Log errors to a file instead
log_errors = On
error_log = /var/log/php/php-errors.log
; Report everything except deprecation notices on production
error_reporting = E_ALL & ~E_DEPRECATED
; Don't include HTML links to the PHP manual in errors
html_errors = Off
Create the log directory and give it to the user PHP-FPM runs as (often www-data on Debian/Ubuntu):
sudo mkdir -p /var/log/php
sudo chown www-data:www-data /var/log/php
sudo chmod 750 /var/log/php
Make sure the log file lives outside your web root, so nobody can download it through the browser. If you run WordPress, keep WP_DEBUG_DISPLAY set to false on production too, since WordPress can override PHP's display setting at runtime.
Step 5: Disable Dangerous Functions
Some PHP functions let code run shell commands or spawn processes. Legitimate applications rarely need them, but web shells uploaded by attackers rely on them heavily. The disable_functions directive lets you turn them off entirely.
; Block functions that execute system commands or spawn processes
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,show_source
A few important notes:
-
Test before you commit: Some plugins, image libraries, backup tools, or deployment scripts do call
exec()orproc_open(). Check your error log after enabling this, and remove a function from the list only if something you trust genuinely needs it. -
Treat CLI separately: WP-CLI, Composer, and some cron scripts need process functions. Apply
disable_functionsto the FPM configuration and leave the CLI configuration more permissive, since only trusted users run CLI commands. -
It's not a complete sandbox: Disabling functions raises the bar, but it isn't bulletproof. It works best alongside the other settings in this guide and proper file permissions.
-
Know what you're disabling: Avoid copying huge lists from the internet that include things like
curl_execormail. Disabling those will quietly break updates, API calls, and emails in WordPress.
disable_functions can only be set in php.ini or a pool configuration via php_admin_value, not from .htaccess or ini_set(), which is exactly what you want.
Step 6: Block Remote File Inclusion
Remote file inclusion happens when an application includes a file based on user input, and an attacker supplies a URL pointing to their own server. PHP has a setting that blocks include and require from loading remote URLs.
; Never allow include/require to load code from a URL
allow_url_include = Off
; Allow fopen/file_get_contents to fetch URLs (needed by many apps)
allow_url_fopen = On
allow_url_include has been off by default for a long time and is deprecated in PHP 7.4 and later, but it's worth setting explicitly. allow_url_fopen is different: many applications, including parts of WordPress and popular plugins, use it to fetch remote data. You can turn it off if your code uses cURL exclusively, but test carefully first.
Step 7: Restrict File Access With open_basedir
The open_basedir directive limits the directories PHP scripts can open files in. If a site is compromised, this makes it much harder for the attacker to read other sites' files, system configuration, or credentials stored elsewhere on the server.
; Only allow access to the site's own directory and temp folders
open_basedir = /var/www/example.com/:/tmp/
This setting is most useful when applied per site rather than globally, which you'll see in Step 11. Keep these points in mind:
- Include a trailing slash on each path so
/var/www/example.comdoesn't also match/var/www/example.com-staging. - Include any directory the application legitimately needs, such as a separate uploads location or a session directory.
- Expect a small performance cost on file-heavy applications. It's usually negligible, but it's worth measuring on busy sites.
open_basedir isn't a complete security boundary on its own, so it works best combined with separate system users per site.
Step 8: Harden Session Settings
PHP sessions are identified by a cookie. If an attacker steals or plants that cookie, they can impersonate the user. These settings make session cookies much harder to abuse:
; Only send session cookies over HTTPS
session.cookie_secure = 1
; Prevent JavaScript from reading the session cookie
session.cookie_httponly = 1
; Limit cross-site sending of the cookie
session.cookie_samesite = Lax
; Reject session IDs that the server didn't create
session.use_strict_mode = 1
; Only accept session IDs from cookies, never from URLs
session.use_only_cookies = 1
session.use_trans_sid = 0
; Use longer, more random session IDs
session.sid_length = 48
session.sid_bits_per_character = 6
Note that session.sid_length and session.sid_bits_per_character are deprecated in PHP 8.4, where the defaults are already strong. On 8.4 and newer, you can leave those two lines out to avoid deprecation warnings.
WordPress itself uses its own authentication cookies rather than PHP sessions, but many plugins, shopping carts, and custom applications rely on PHP sessions, so these settings are still worth applying. Only enable session.cookie_secure once your whole site runs on HTTPS.
Step 9: Set Sensible Resource Limits
Resource limits stop a single request from consuming all your server's memory or running forever. They also reduce the impact of oversized uploads and some denial-of-service attempts.
; Maximum time a script can run (seconds)
max_execution_time = 60
; Maximum time spent parsing input data (seconds)
max_input_time = 60
; Memory a single script may use
memory_limit = 256M
; Upload and POST size limits
upload_max_filesize = 32M
post_max_size = 40M
max_file_uploads = 20
; Limit the number of input variables per request
max_input_vars = 3000
These values are starting points, not rules. A WooCommerce store importing large product files or a site processing big images may need more. The key is to set limits deliberately, based on what your applications need, rather than setting everything to huge values "just in case". post_max_size should always be slightly larger than upload_max_filesize.
If you don't accept uploads at all, you can turn them off entirely:
file_uploads = Off
That's rarely practical for a CMS, but it's a great option for API-only services.
Step 10: Other Useful Hardening Directives
A few more settings are worth reviewing:
; Don't let PHP guess the script path (important with some Nginx setups)
cgi.fix_pathinfo = 0
; Enable OPcache for performance, but validate file changes
opcache.enable = 1
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2
; Restrict OPcache's API to your site's files (per pool is best)
; opcache.restrict_api = /var/www/example.com/
cgi.fix_pathinfo = 0 is a historical safeguard against a class of Nginx misconfigurations where a request like /uploads/image.jpg/index.php could cause PHP to execute an uploaded file. Modern PHP-FPM packages and correctly written Nginx configs handle this with security.limit_extensions and try_files, but setting it doesn't hurt most applications.
In your PHP-FPM pool file (for example /etc/php/8.3/fpm/pool.d/www.conf), also make sure only .php files can be executed:
security.limit_extensions = .php
Step 11: Apply Settings Per Site With PHP-FPM Pools
On a server hosting several sites, the best approach is a separate PHP-FPM pool for each site, each running as its own system user with its own restrictions. If one site is compromised, the others stay protected.
Create a pool file such as /etc/php/8.3/fpm/pool.d/example.com.conf:
[example.com]
user = example
group = example
listen = /run/php/php8.3-fpm-example.com.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s
; Only allow .php files to be executed
security.limit_extensions = .php
; Per-site hardening (cannot be overridden by the application)
php_admin_value[open_basedir] = /var/www/example.com/:/tmp/
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,show_source
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php/example.com-error.log
php_admin_flag[allow_url_include] = off
php_admin_value[session.save_path] = /var/lib/php/sessions/example.com
A few details matter here:
php_admin_valuevsphp_value: Values set withphp_admin_valueandphp_admin_flagcan't be changed by the application usingini_set(). Use them for security settings.- Separate session directories: Giving each site its own session path stops one site from reading another's session files. Create the directory and make it owned by the site user with
chmod 700. - Point your web server at the new socket: In Nginx, update
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;in that site's server block.
Create the session directory:
sudo mkdir -p /var/lib/php/sessions/example.com
sudo chown example:example /var/lib/php/sessions/example.com
sudo chmod 700 /var/lib/php/sessions/example.com
Step 12: Test and Reload PHP
After editing configuration files, test PHP-FPM's syntax before restarting, so a typo doesn't take all your sites offline:
sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpm
On RHEL-based systems, the equivalent is usually sudo php-fpm -t followed by sudo systemctl reload php-fpm. If you use Apache with mod_php, run sudo apachectl configtest and then sudo systemctl reload apache2.
To confirm the settings the web server actually uses, create a temporary file in the site's web root:
<?php
// check-ini.php - delete this file right after using it.
header( 'Content-Type: text/plain' );
foreach ( array( 'expose_php', 'display_errors', 'open_basedir', 'disable_functions', 'session.cookie_secure' ) as $key ) {
echo $key . ' = ' . var_export( ini_get( $key ), true ) . "\n";
}
Load it once in your browser, check the output, and delete it immediately. Leaving diagnostic files like this, or a phpinfo() file, on a live site is a common way servers leak information.
You can also check the response headers from your own machine to confirm X-Powered-By is gone:
curl -sI https://example.com | grep -i x-powered-by
No output means the header is no longer sent.
What About Shared Hosting?
On shared hosting you usually can't edit the main php.ini or create FPM pools. You still have options:
- Use your control panel: cPanel's MultiPHP INI Editor and similar tools in Plesk or other panels let you change common directives per domain.
- Use a
.user.inifile: With PHP-FPM or CGI, PHP reads.user.inifiles in your site's directories for settings that are allowed to be changed per directory, such asdisplay_errors,upload_max_filesize, and session options. - Ask your host: Settings like
disable_functionsandopen_basedirare controlled by the host. A good host will already have sensible restrictions in place and should be willing to explain them.
A .user.ini example:
display_errors = Off
log_errors = On
session.cookie_secure = 1
session.cookie_httponly = 1
Remember to block web access to .user.ini and other dotfiles. Most hosts already do this, but it's worth checking by requesting the file in your browser.
A Quick PHP Hardening Checklist
Before you move on, run through this list:
- Supported version: You're on a PHP 8.x branch that still receives security fixes.
- No version leak:
expose_php = Off. - No error display:
display_errors = Offand errors are logged outside the web root. - Dangerous functions disabled: Process and shell functions are blocked for web requests.
- No remote includes:
allow_url_include = Off. - File access restricted:
open_basediris set per site. - Secure sessions: Secure, HttpOnly, SameSite, and strict mode are enabled.
- Reasonable limits: Memory, execution time, and upload limits match real needs.
- Per-site isolation: Each site has its own FPM pool and system user.
- No leftover test files: No
phpinfo()or diagnostic scripts remain on the server.
FAQ: Securing PHP Settings
On Ubuntu and Debian, each PHP version and SAPI has its own file, such as /etc/php/8.3/fpm/php.ini for PHP-FPM and /etc/php/8.3/cli/php.ini for the command line. Run php --ini to see which file the CLI loads, and check your web server's FPM version for the one your site uses.
WordPress core doesn't need shell functions like exec or system, so disabling them rarely breaks core. Some backup, image optimisation, or deployment plugins do use them, so check your error log after the change and adjust only if a trusted plugin needs a specific function.
No. Setting expose_php = Off only removes an easy clue for scanners. It's worth doing, but real protection comes from running a supported PHP version, keeping applications updated, and applying the other restrictions in this guide.
Only on local development or a private staging environment. On a live site, errors should always be logged to a file outside the web root so visitors and attackers never see file paths or code details.
Both set PHP directives in a PHP-FPM pool or Apache config, but values set with php_admin_value or php_admin_flag can't be overridden later by the application using ini_set(). Use the admin versions for security-related settings.
Partly. You can usually change display, upload, and session settings through your control panel or a .user.ini file. Server-level settings like disable_functions and open_basedir are controlled by the host, so choose a host that already applies them.
Yes. Reload PHP-FPM (for example sudo systemctl reload php8.3-fpm) or reload Apache if you use mod_php. Test the configuration first with php-fpm8.3 -t so a typo doesn't take your sites offline.
Conclusion
Securing PHP settings is one of the most cost-effective hardening steps you can take on a web server. A handful of directives in a dedicated override file can hide version details, keep errors out of the browser, block shell access from web requests, stop remote code inclusion, confine each site to its own directory, and make session cookies far harder to steal.
Start with a backup of your current configuration, apply the changes gradually, and watch your error logs as you go so you catch anything a plugin genuinely needs. Once your baseline is in place, revisit it whenever you upgrade PHP or add a new application, and your server will be a much less rewarding target when something eventually goes wrong higher up the stack.


