In short: With expose_php = Off in php.ini, the header X-Powered-By: PHP/… disappears. To go with it: server_tokens off; in Nginx and ServerTokens Prod and ServerSignature Off in Apache. This does not make a server more secure, but it takes a ready-made version list away from automated scans. You then check your own patch level via package management and processes, not via headers.
The business card in every response
Anyone who wanted to know after 21 October 2026 how many servers still show an old PHP version did not have to search long. Many web servers state their PHP version in every single response, for example as X-Powered-By: PHP/8.2.20. Internet-wide search services read this information along, and Unit 42 of Palo Alto Networks counted more than 22.6 million exposed, potentially vulnerable instances for PHP2Shell.
For attackers, such information is a shortcut. They can pre-sort targets instead of trying blindly. For operators it is useless, because it does not even prove their own patch level: distributions often ship fixes as backports without a new version number.
This article shows where PHP and web servers reveal version information and how to switch it off. It also says openly what this achieves and what it does not.
Where version information appears
The PHP version is only one of several places. A complete look is worthwhile, because a single forgotten place devalues the other measures.
| Where found | Example | Switch off via |
|---|---|---|
| HTTP header from PHP | X-Powered-By: PHP/8.3.x |
expose_php = Off in php.ini |
| HTTP header of the web server | Server: nginx/1.x or Server: Apache/2.4.x (Debian) |
server_tokens off; or ServerTokens Prod |
| Error pages of the web server | version line below 404 or 500 pages | the same settings, in Apache additionally ServerSignature Off |
| Application headers | custom X-Powered-By entries from frameworks |
fastcgi_hide_header or Header unset |
| Files in the web root | phpinfo() test files, readme and changelog files |
delete or block |
| PHP error messages | paths and version details in the browser | display_errors = Off |
Test files with phpinfo() are the richest source for attackers. They reveal not only the version but the entire configuration; more on this in the article phpinfo() files: why they have to go.
Disabling expose_php in php.ini
According to the PHP manual, expose_php is enabled by default and can only be changed in php.ini, not in the pool, not via .htaccess and not at runtime. Set the value in the php.ini of every operating mode that serves websites, so on Debian and Ubuntu for example under /etc/php/<version>/fpm/ and, where applicable, /etc/php/<version>/apache2/.
; php.ini of the web SAPI, then restart PHP-FPM or Apache
expose_php = Off
If several PHP versions run in parallel, each needs its own entry. Restarting the respective service is mandatory, because PHP reads php.ini only at startup.
Shortening server headers in Nginx and Apache
By default, Nginx states its version in the Server header and on error pages. With server_tokens off; only the product name remains. In addition, an X-Powered-By header set by the application itself can be filtered out with fastcgi_hide_header. According to the Nginx documentation, Nginx hides only Status and X-Accel-… on its own, not X-Powered-By.
# in the http block, applies to all websites
server_tokens off;
fastcgi_hide_header X-Powered-By;
In Apache, ServerTokens controls the Server header and at the same time the version line below error pages. According to the Apache documentation, the setting applies server-wide only, not per VirtualHost. Because PHP-FPM responses via mod_proxy_fcgi end up in a different header table than responses from the server itself, apply the removal in both variants.
# server-wide configuration, e.g. /etc/apache2/conf-available/security.conf
ServerTokens Prod
ServerSignature Off
# requires mod_headers
<IfModule mod_headers.c>
Header unset X-Powered-By
Header always unset X-Powered-By
</IfModule>
Before reloading, check the configuration with nginx -t or apachectl configtest. How to actively switch on protection for visitors via headers, for example with HSTS or a Content Security Policy, is described in the guide Security headers for websites.
How to check the result from outside
After the changeover, check each domain individually, if possible including an error page. The following commands send only normal requests to your own websites:
# Headers of the home page
curl -sI https://www.example.de/ | grep -iE '^(server|x-powered-by):'
# Headers of a non-existent page
curl -sI https://www.example.de/gibt-es-nicht-4711 | grep -iE '^(server|x-powered-by):'
The expected result is a Server header without a version number and no X-Powered-By. If a PHP version still appears, usually a different php.ini is active or a service has not been restarted yet. If a CDN sits in front of the server, additionally check the origin server directly, as far as it is reachable.
Still record the PHP version internally: in your server inventory, with package state and the date of the last check. You prove the patch level through the server, as described under Checking PHP version and patch level correctly.
What hiding achieves and what it doesn't
Put honestly, switching off version information is a small measure with little effort. It lowers the probability of showing up in targeted searches for particular versions and reduces the information an attacker gets without effort.
But it does not protect against automated attacks that simply send their requests to every reachable server. How such mass scans work is described in the article Mass scans for unpatched PHP. Against PHP2Shell, only the update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or a confirmed backport, helps, followed by a restart of all PHP processes.
One side effect, on the other hand, is quite useful: anyone who switches off version information to the outside must record their versions cleanly on the inside. In a situation like PHP2Shell, exactly this overview decides how quickly you know which servers are affected.
Hiding is therefore sensible as part of basic hardening, not as a single measure. It belongs on the same checklist as disabled error output, deleted test files and locked configuration values.
How does PHP SOS help?
- Record where information appears: We check headers, error pages and web root of all domains for version information and forgotten test files.
- Adjust configuration: We set
expose_php, server headers and header filters in every PHP version and every web server and restart the services in a controlled way. - Prove the patch level internally: We document versions and package states where they belong, in the inventory rather than in the HTTP header.
- Embed in hardening: The measure is part of our server hardening for PHP web servers.
Frequently asked questions
Does expose_php = Off make my server secure?
No. The setting only removes the version information from the HTTP header. A vulnerable PHP version stays vulnerable, and automated attacks often try their requests on every reachable server anyway. It does, however, take away an easy pre-selection for attackers and lowers visibility in search engines for servers.
Can I set expose_php in the FPM pool or via .htaccess?
According to the PHP manual, expose_php can only be changed in php.ini. Set the value in the php.ini of every web SAPI and then restart PHP-FPM or Apache.
Why do I still see X-Powered-By despite expose_php = Off?
Then usually the application or a framework sets the header itself, or a different php.ini is active. Additionally remove the header in the web server: with fastcgi_hide_header in Nginx or Header unset in Apache.
How do I check my patch level if the version is no longer visible?
Through the server itself: package management, the package changelog with the CVE numbers and the start time of the PHP processes. The information in the HTTP header was never proof anyway, because distributions often ship backports without a new version number.