Remediation Guides

PHP expose_php Information Disclosure: How to Set expose_php Off

26 September 2026 8 min read

PHP expose_php Information Disclosure means PHP is announcing itself: it adds an X-Powered-By: PHP/x.y header to responses and, on PHP 5.4 and older, answers special Easter egg URLs. To fix it, set expose_php = Off in the php.ini your web server actually loads, restart PHP-FPM or the web server, then rescan.

What the scanner is actually detecting

The expose_php directive is on by default. Even the php.ini-production template shipped with PHP contains expose_php = On, so almost every fresh install triggers this finding. Two scanners report it under different names:

  • Nessus plugin 46803, “PHP expose_php Information Disclosure”, a remote check in the Web Servers family. Tenable describes it as PHP exposing information through a special URL that triggers an Easter egg built into PHP, and rates it Medium.
  • ZAP alert 10037, “Server Leaks Information via “X-Powered-By” HTTP Response Header Field(s)”, a passive rule rated Low (CWE-497). It fires on any X-Powered-By header, whether PHP, ASP.NET, Express or something else added it.

The two checks look at different things, and that matters when you verify the fix:

Behavior controlled by expose_php PHP versions Reported by
X-Powered-By: PHP/x.y response header All versions ZAP 10037 and similar header checks
Easter egg query strings such as ?=PHPE9568F34-D428-11d2-A769-00AA001ACF42 returning the PHP logo, and ?=PHPB8B5F2A0-3C92-11d3-A3A9-4C7B08C10000 returning the PHP credits page 5.4 and older only Nessus 46803

In PHP 5.4 the handler for these query strings only runs when expose_php is enabled. PHP 5.5 removed the logo GUIDs entirely, as listed in the PHP 5.5 UPGRADING notes. On current PHP, expose_php controls only the X-Powered-By header.

Real-world risk

This is an information disclosure finding, not a way into the server. The header tells an attacker that PHP runs on the host and which version, which lets them match it against published CVEs without probing. The Easter egg URLs reveal the same kind of information. PHP’s own comment in php.ini-production says the setting is “no security threat in any way”, and that is fair for the header on its own.

The finding deserves attention for a different reason. If Nessus 46803 fires, the host is very likely answering the Easter egg URL, which only PHP 5.4 or older does. RHEL 7 and CentOS 7, for example, ship PHP 5.4.16 in their base repositories. Turning expose_php off closes the finding, but the old PHP branch behind it is the real problem. Keep in mind that distribution packages backport fixes without changing the version string, which is explained in why scanners flag backported patches as vulnerable.

How to confirm it on the host

Start from outside. Request a URL that is handled by PHP, not a static file:

curl -s -D - -o /dev/null https://www.example.com/index.php | grep -i x-powered-by

To reproduce the Nessus check on older PHP, request the logo GUID. A response of image/gif means the Easter egg is live; PHP 5.5 and later return your normal page instead.

curl -s -o /dev/null -w "%{http_code} %{content_type}n" 
  "https://www.example.com/index.php?=PHPE9568F34-D428-11d2-A769-00AA001ACF42"

Then find the php.ini the web server really loads. The CLI, PHP-FPM and mod_php often read different files, so checking only php –ini can mislead you.

# CLI (shows loaded php.ini and the scanned conf.d directory)
php --ini

# PHP-FPM on Debian/Ubuntu (binary name includes the version)
php-fpm8.3 -i | grep -iE "Loaded Configuration File|expose_php"

# PHP-FPM on RHEL, Rocky, AlmaLinux
php-fpm -i | grep -iE "Loaded Configuration File|expose_php"

# Search every ini file for the directive
grep -Rn "expose_php" /etc/php/ 2>/dev/null           # Debian/Ubuntu
grep -Rn "expose_php" /etc/php.ini /etc/php.d/ 2>/dev/null   # RHEL family

On Windows with IIS, PHP usually reads php.ini from its install directory:

C:PHPphp.exe --ini
Select-String -Path C:PHPphp.ini -Pattern '^s*expose_php'

How to fix it: set expose_php Off

The PHP manual lists expose_php as changeable in php.ini only, so ini_set() in application code cannot change it. Edit the ini file the web SAPI loads.

Debian and Ubuntu with PHP-FPM

sudo sed -i 's/^s*expose_phps*=.*/expose_php = Off/' /etc/php/8.3/fpm/php.ini
grep -n "^expose_php" /etc/php/8.3/fpm/php.ini
sudo systemctl restart php8.3-fpm

Replace 8.3 with your version, and repeat for each installed version that serves traffic. If you use mod_php instead of FPM, edit /etc/php/8.3/apache2/php.ini and restart Apache with sudo systemctl restart apache2.

RHEL, Rocky, AlmaLinux and CentOS

These distributions use a single /etc/php.ini plus drop-in files in /etc/php.d/. Set the value, confirm no drop-in turns it back on, then restart whichever process runs PHP:

sudo sed -i 's/^s*expose_phps*=.*/expose_php = Off/' /etc/php.ini
grep -Rn "expose_php" /etc/php.ini /etc/php.d/
sudo systemctl restart php-fpm     # PHP-FPM
sudo systemctl restart httpd       # mod_php, for example PHP 5.4 on RHEL 7

Windows and IIS

Open php.ini in the PHP directory, set expose_php = Off, then recycle the application pool or run iisreset in a maintenance window so the FastCGI php-cgi.exe processes restart and reread the file. IIS also adds its own X-Powered-By: ASP.NET header by default in applicationHost.config, which ZAP 10037 reports separately. Remove it in the site’s web.config:

<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Defense in depth: strip the header at the web server

Application code can still send X-Powered-By with header(), and new PHP versions installed later start with expose_php On again. Stripping the header at the front end catches both cases.

On nginx with PHP-FPM, fastcgi_hide_header is valid in http, server and location context. For backends reached through proxy_pass, use proxy_hide_header instead:

http {
    fastcgi_hide_header X-Powered-By;
    proxy_hide_header X-Powered-By;
}
sudo nginx -t && sudo systemctl reload nginx

If a server or location block already defines its own fastcgi_hide_header lines, add X-Powered-By there as well, because a block with its own list does not inherit the list from the level above.

On Apache, use mod_headers (on Debian/Ubuntu enable it with sudo a2enmod headers). The Apache documentation notes that headers produced by a CGI script or by mod_proxy_fcgi sit in the always table, not the default one, so set both forms to cover mod_php and PHP-FPM:

<IfModule mod_headers.c>
    Header unset X-Powered-By
    Header always unset X-Powered-By
</IfModule>
sudo apachectl configtest && sudo systemctl reload apache2   # or httpd

How to verify the fix and rescan

  1. Rerun php-fpm8.3 -i | grep expose_php (or the equivalent for your SAPI) and confirm it shows Off.
  2. Repeat the curl requests against a PHP page on every virtual host and port. The X-Powered-By line should be gone, and the Easter egg request should no longer return image/gif.
  3. If a CDN or caching proxy sits in front, purge it or test the origin directly so you do not read cached headers.
  4. Rescan with the tool that raised the finding. ZAP 10037 is passive, so any fresh crawl through ZAP re-evaluates it. For Nessus, rerun the scan policy that includes plugin 46803 against the same target.

What can break and how to roll back

Disabling expose_php does not change how PHP runs your code, so application breakage is very unlikely. The practical side effects are:

  • Monitoring or inventory tools that read X-Powered-By to detect PHP versions lose that data. Take versions from the package manager or an authenticated scan instead, as described in credentialed vs uncredentialed scanning.
  • Restarting PHP-FPM or Apache drops in-flight requests on busy servers, so schedule it.
  • A typo in php.ini or an Apache or nginx config can stop the service from starting. Always run nginx -t or apachectl configtest before reloading.

To roll back, set expose_php = On and restart PHP, and remove the fastcgi_hide_header, proxy_hide_header or Header unset lines, then reload the web server. Keep a copy of each file before editing, for example sudo cp /etc/php.ini /etc/php.ini.bak.

Common false positive reasons

  • The header comes from somewhere else. ZAP 10037 reports any X-Powered-By header. IIS (ASP.NET), Node.js frameworks, a CDN or the application itself may add one after PHP stops.
  • The wrong php.ini was edited. The CLI file was changed but PHP-FPM or mod_php loads another one, or a conf.d file later in the load order sets expose_php back to On.
  • Another virtual host or port. A second site, an older PHP version on the same IP, or an admin panel on a different port still has the default setting.
  • Nessus 46803 on modern PHP. If the host runs PHP 5.5 or later and the logo request returns your normal page, collect the plugin output and challenge the result, since the Easter egg no longer exists there.
  • Stale results. The report predates the change, or a cache served old headers.

FAQ

Is expose_php a serious vulnerability?

No. It discloses that PHP is installed and which version. The finding matters most when the disclosed version is outdated, which is a patching problem, not a header problem.

Can I disable expose_php in .htaccess or with ini_set()?

No. The PHP manual lists it as changeable in php.ini only. Use the php.ini loaded by your web SAPI, or strip the header at the web server.

Does expose_php = Off hide PHP completely?

No. File extensions, session cookie names such as PHPSESSID and error messages can still reveal PHP. It removes one easy signal, nothing more.

Why is Nessus 46803 still reported after the change?

Usually PHP was not restarted, the wrong ini file was edited, or another virtual host still runs with the default. Rerun the Easter egg curl request against each site the scanner lists.

Tracking this finding across many hosts

On a large estate the hard part is knowing which web servers are really fixed. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners and merges duplicates reported by the same scanner (not across different scanners). Its AI can draft a host-specific script, for example editing php.ini and restarting PHP-FPM, that runs only after human approval and is deployed by SITEY agents on Windows and Linux endpoints. For Nessus findings such as 46803 it can retest the individual finding; for other scanners, import a fresh scan to confirm closure.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing