Current situation: PHP2Shell. Two critical PHP vulnerabilities (CVSS 9.2) allow malicious code without login. Patches since 21 October 2026.What to do now →

expose_php and Server Headers: Hiding Version Information

PHP, Nginx and Apache state their version number in every response by default. This does not protect against attacks, but gives scanners a handy hit list. Here is how to switch it off and still keep track.

Last updated 6 min readHardening and configuration

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.

Tip

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.

Sources

Immediate contact

Every hour counts. Write to us now.

Briefly describe your system, PHP version and observation. We will get back to you, clarify the situation and work remotely until the incident is closed and documented.

Immediate help