X-Powered-By: what that header gives away
Published · updated
One response header names your PHP build to every visitor, every crawler and every bot that scans for last week's vulnerability. Here is how to read it, how to remove it, and what removing it does not do.
What the header contains
$ curl -sI https://example.com/ | grep -i x-powered-by X-Powered-By: PHP/8.3.29
That is the exact build serving the response — not a guess, not a range. With that one line an attacker knows three things: the site runs PHP, which branch, and whether it is older than the fix published last week. Combined with a public advisory, it turns “scan everything” into “patch-level targeting”.
The same header on ASP.NET sites reads ASP.NET, on Express it reads Express. It is a framework banner, and PHP has shipped it by default for decades through the expose_php setting.
Where else the version leaks
Removing one header while leaving a worse leak open is the usual mistake. These publish the build too:
- A reachable
phpinfo(). The full page — build, extensions, ini paths, environment. The most complete disclosure there is. - A PHP-FPM status page. Pool status exposes request URIs, process counts and often the pool configuration.
- Error pages with display_errors on. Stack traces include file paths and sometimes the build.
- Debug toolbars, composer and framework exception pages left enabled in production.
- Static assets with build strings in filenames or comments — weaker, but a confirmation.
Our checker looks for all of these separately, because a site with no X-Powered-By and an open /phpinfo.php is not less exposed than one that prints the header.
How to remove it
In PHP itself — the only place that covers every SAPI at once:
; php.ini, then reload FPM or Apache
expose_php = Off
Apache, when PHP sets the header anyway:
<IfModule mod_headers.c> Header unset X-Powered-By </IfModule>
nginx — needs headers-more for more_clear_headers, or a proxy_hide_header when PHP is behind a proxy:
more_clear_headers 'X-Powered-By'; proxy_hide_header X-Powered-By;
FPM and PHP-FPM pools inherit php.ini; if the pool has its own php_admin_value block, check that it does not set expose_php = On.
Then verify with curl -sI on the main page and on a .php URL, and again after any deploy — some framework or plugin sets the header back at runtime.
What removing it does not do
It does not patch anything. The vulnerability is in the interpreter, not in the banner. Hiding the version changes who notices you first; it does not change what happens when someone decides to try. The two bugs published on 21 October 2026 — CVE-2026-109618 and CVE-2026-109619 — are exploitable regardless of whether your header is visible.
Removing it also costs you something real: the same header is how an outside check, a monitoring service or a client report can confirm that a patch landed. Sites that hide the version need the server command as their proof. That is a fair trade — as long as someone actually runs the command.
Questions
Does hiding X-Powered-By improve security at all?
Marginally, and only against automated sweeps that pick targets by banner. It does nothing against an attacker who already chose you. Treat it as tidiness, not protection.
I removed it but the header is back after deployment.
A runtime component is setting it — a framework header, a proxy in front, or a container image with its own php.ini. Test a .php URL rather than only the static homepage, and check the ini file the running process loads.
Should I hide the version from my own monitoring?
Keep monitoring on a path you control or alert on the build reported by the server itself. Hiding it from the internet and losing the ability to verify a patch are different decisions.
What should the Server header look like?
It can stay generic (nginx, Apache) — the risk there is the version of the web server, which is a separate patch stream. The PHP build in X-Powered-By is the one that maps directly to this month's advisories.