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¶
- Set
display_errors = On, write a PHP file with a deliberate error, load it, read what leaks. Set it Off. - Run
php -i | grep expose_phpbefore 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.