Skip to content

PHP

A web site written in PHP runs code on the server for every request, and php.ini decides how much that code can do and how much it tells attackers when it fails. The file is per-version and per-server: /etc/php/8.1/apache2/php.ini for Apache on Mint 21, /etc/php/8.2/apache2/php.ini on Debian 12, …/fpm/php.ini for nginx with PHP-FPM, …/cli/php.ini for the command line. Fix the one the web server uses; php -v shows the version, ls /etc/php/ lists them.

The settings

Setting Value Why
display_errors Off Error messages show file paths, SQL queries, and sometimes credentials to whoever caused the error
display_startup_errors Off Same, for startup
log_errors On, with error_log = /var/log/php_errors.log Errors go to a log for the administrator instead
expose_php Off Removes the X-Powered-By: PHP/8.3 header
allow_url_include Off include($_GET['page']) can't load code from a remote URL (remote file inclusion)
allow_url_fopen Off, unless the application fetches URLs Same family; scripts can't open remote files
open_basedir /var/www/html:/tmp (the web root and a temp dir) PHP can't read files outside these paths, so a path-traversal bug can't reach /etc/passwd
disable_functions exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,dl The functions a web shell uses to run commands are turned off
session.cookie_httponly 1 JavaScript can't read the session cookie (blocks cookie theft by XSS)
session.cookie_secure 1 The session cookie is only sent over HTTPS
session.use_only_cookies 1 The session ID can't be passed in the URL, where it ends up in logs and referers
session.use_strict_mode 1 The server won't accept a session ID it didn't issue (session fixation)
register_globals Off (removed in PHP 5.4; on an old image, make sure it's Off) Request parameters became variables, which let attackers set anything
file_uploads Off, unless the application uploads
max_execution_time 30 A runaway script can't hold a worker forever

Apply

Find the file first:

php -v | head -1
ls /etc/php/

Then open it, sudo nano /etc/php/8.1/apache2/php.ini (or whatever version and server you found), and use Ctrl+W to search for each directive in the table. Lines that start with ; are comments showing the default; uncomment the line and set the value, or add a new line if the directive isn't there. A few of them look like this when done:

display_errors = Off
expose_php = Off
allow_url_include = Off
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,dl

The file is long and a directive can appear twice, once commented and once set. The last uncommented one wins, so after editing, check each:

grep -nE '^\s*;?\s*(display_errors|expose_php|allow_url_include|open_basedir|disable_functions)\s*=' /etc/php/8.1/apache2/php.ini

That prints every occurrence with its line number, commented or not. When it's right, restart the web server: sudo systemctl restart apache2 (or php8.1-fpm for nginx).

Twelve directives by hand is enough repetitions to see the pattern. A sed one-liner that uncomments and sets one directive, sudo sed -i -E 's/^;?\s*expose_php\s*=.*/expose_php = Off/' "$F", is the building block of a script that does all twelve; write it once you've done them by hand and understand what each line must end up as.

Verify

php -i 2>/dev/null | grep -E '^(display_errors|expose_php|allow_url_include|open_basedir|disable_functions|session.cookie_httponly|session.cookie_secure|session.use_only_cookies)'
curl -sI http://localhost/index.php | grep -i powered

php -i reads the CLI ini; for the web server's actual values, put <?php phpinfo(); in a temporary file under the web root, load it in a browser, and delete it afterward. The curl should print no X-Powered-By line.

Example

grep -E '^(display_errors|allow_url_include|disable_functions)' /etc/php/8.3/apache2/php.ini shows display_errors = On, allow_url_include = On, and ;disable_functions = commented out. That's a site that prints its errors, includes remote code, and can run shell commands. Apply the block above, restart Apache, and grep the web root for system( and eval( to see whether anyone already used it.

Try it

  1. Set display_errors = On, write a PHP file with a deliberate error, load it, read what leaks. Set it Off.
  2. Run php -i | grep expose_php before and after.

Build it

A php-check.sh that prints the uncommented value of each directive on this page from the web server's php.ini.

Next

MySQL and MariaDB