Fix Guide

How to Hide Your PHP Version from HTTP Response Headers

April 19, 2026

Open your browser's network tab on any PHP based website and look at the response headers for the main HTML document. On a lot of installations you will find a header that reads something like X-Powered-By: PHP/8.1.27. That is PHP helpfully telling the entire internet which version it is running. It is a tiny detail that most site owners never notice, but it happens to be one of the easier pieces of information an attacker collects before deciding whether a site is worth probing further.

This guide walks through why the header exists in the first place, what it actually exposes, and every realistic way to get rid of it. From the server config down to the level of a single WordPress site on shared hosting where you cannot touch the php.ini.

What the header actually tells an attacker

A value like PHP/8.1.27 reveals two useful facts. First, the major version. If you are still on PHP 7.4 or PHP 8.0, both of which reached end of life, an attacker immediately knows that no security patches are coming. Second, the exact patch level. Between any two patch releases there are usually a handful of publicly known CVEs. A scanner can compare your reported version against its CVE database and decide in milliseconds whether to keep poking or move on.

Hiding the version does not patch anything, obviously. If your PHP is outdated, it is still outdated. But removing the broadcast does three concrete things: it stops automated bulk scanners from flagging your site as a cheap target, it makes manual reconnaissance slightly more expensive, and it removes a piece of information that simply has no business being in your response headers. There is no legitimate reason for the public internet to know your PHP patch level.

Some compliance frameworks and security checklists (PCI-DSS, BSI IT-Grundschutz, internal audits in larger companies) explicitly ask for this header to be suppressed. Even if you do not work in one of those contexts, it is a five minute fix with zero downside.

Where does the header come from?

The X-Powered-By header is emitted by PHP itself, not by your web server. The behavior is controlled by a single php.ini directive called expose_php. When that directive is set to On (which is the default in most PHP distributions), PHP adds the header to every response automatically. Turn it off and the header disappears for every site on that PHP installation.

On top of that, some frameworks or applications add their own X-Powered-By values. Express, ASP.NET, and others do this. WordPress itself does not, but plugins occasionally do. So after you turn off PHP's version disclosure, it is worth checking the response headers one more time to see if anything else is still leaking.

Option 1: Disable expose_php in php.ini

This is the cleanest fix if you control the server. Open your php.ini file. On Linux the path is usually one of these:

  • /etc/php/8.3/fpm/php.ini for PHP-FPM, the most common setup behind nginx and modern Apache
  • /etc/php/8.3/apache2/php.ini for Apache with mod_php
  • /etc/php/8.3/cli/php.ini for the command line, which you usually do not care about in this context

The version number in the path will match whatever PHP you are on. Find the line that reads:

expose_php = On

Change it to:

expose_php = Off

Save the file and restart the PHP process so the change takes effect. For PHP-FPM that looks like:

sudo systemctl restart php8.3-fpm

For Apache with mod_php:

sudo systemctl restart apache2

To verify, run a quick curl against your site and look at the headers:

curl -I https://yourdomain.com

The X-Powered-By line should be gone completely. If it is still there, you edited the wrong php.ini file (there is often more than one). Check which one is actually loaded with php --ini on the command line, or by putting a temporary phpinfo() call on the site (and removing it immediately afterwards, please).

Option 2: Set expose_php per site via .user.ini

Not everyone has access to the global php.ini. On many shared hosts you can still influence PHP settings through a file called .user.ini, which you place in the document root of your site. Create (or edit) a .user.ini with:

expose_php = Off

PHP checks this file on every request, but it caches the result for a few minutes by default (controlled by user_ini.cache_ttl, usually 300 seconds). So give it five minutes, clear any edge caches, and then check your headers again.

A word of caution: not all hosts allow expose_php to be overridden from .user.ini. The directive has the mode PHP_INI_PERDIR, which technically permits it, but some hosts explicitly whitelist what can be changed. If the setting has no effect after a few minutes, your host is probably blocking it. Skip to Option 5.

Option 3: Remove the header in Apache

You can also strip the header at the web server level, which works no matter what PHP is doing internally. In your Apache config (or in the .htaccess of the site, if AllowOverride permits it), add:

<IfModule mod_headers.c>
  Header unset X-Powered-By
  Header always unset X-Powered-By
</IfModule>

The reason for using both unset and always unset is that the regular version only applies to successful 2xx responses. The always variant also applies to error responses like 500 pages, which is exactly where you would not want the version to leak either.

mod_headers has to be enabled for this to work. On Debian and Ubuntu:

sudo a2enmod headers
sudo systemctl reload apache2

If you are on shared hosting with Apache, the .htaccess variant almost always works because most hosters enable mod_headers and allow it via AllowOverride All. If the rule has no effect, ask your host whether mod_headers is available.

Option 4: Remove the header in nginx

On nginx the equivalent goes into the server block of your site, or into a general http block if you want it site wide:

more_clear_headers 'X-Powered-By';

Small catch: more_clear_headers requires the headers-more-nginx-module, which is not part of the standard nginx package on every distribution. Debian and Ubuntu ship it in the nginx-extras package:

sudo apt install nginx-extras

If you are stuck on plain nginx without the extras module, there is a workaround using fastcgi_hide_header. This is actually the right tool if the header is coming from PHP-FPM (which in most cases it is):

location ~ \.php$ {
    # your usual fastcgi_pass and params
    fastcgi_hide_header X-Powered-By;
}

Reload nginx with sudo nginx -t && sudo systemctl reload nginx and verify with curl -I.

Option 5: Special case, WordPress

WordPress itself does not set X-Powered-By. The header comes from PHP. So everything above applies unchanged: turning off expose_php or stripping the header in Apache or nginx will also solve it for a WordPress site. But WordPress gives you two extra angles that are worth knowing about.

Suppress the header from inside WordPress. If you have no access to the web server config and .user.ini does not work on your host, you can drop the header programmatically through a must use plugin. Create a file at wp-content/mu-plugins/remove-powered-by.php (create the mu-plugins folder if it does not exist yet) and put this inside:

<?php
/**
 * Plugin Name: Remove X-Powered-By
 * Description: Strips the X-Powered-By header from every WordPress response.
 */

if (!defined('ABSPATH')) {
    exit;
}

add_action('send_headers', function () {
    if (function_exists('header_remove')) {
        header_remove('X-Powered-By');
    }
});

Two things to be aware of with this approach. First, header_remove() only works if PHP has not started sending the response body yet. The send_headers hook fires at the right moment for every normal WordPress page, so this works for posts, pages, admin and the REST API. It does not help for requests that never reach WordPress, like direct calls to static files or cached pages served by a reverse proxy. Second, some hosts run a reverse proxy (Cloudflare, Varnish, Nginx in front of Apache) that reads the header from the backend response and then forwards it as is. If the header still shows up in your browser after installing this plugin, the proxy is caching it. Clear the cache and, ideally, strip the header at the proxy level too.

WordPress also exposes its own version. While you are at it, you probably want to deal with the <meta name="generator" content="WordPress X.Y.Z"> tag that WordPress puts in the HTML head, and the ?ver=X.Y.Z parameters on CSS and JS files. Those are not the same as the PHP version header, but they leak the exact WordPress version, which is comparably useful to an attacker. The snippet below handles both:

<?php
// Remove the generator meta tag
remove_action('wp_head', 'wp_generator');

// Strip the WordPress version from RSS feeds, admin, etc.
add_filter('the_generator', '__return_empty_string');

// Remove ?ver=X.Y.Z from CSS and JS file URLs
add_filter('style_loader_src', function ($src) {
    return remove_query_arg('ver', $src);
}, 9999);

add_filter('script_loader_src', function ($src) {
    return remove_query_arg('ver', $src);
}, 9999);

Note that stripping the ?ver= parameter has a side effect: the browser can no longer tell when a CSS or JS file has changed based on the URL alone. If you rely on that for cache busting, either leave the filter out or replace ver with a hash of the file contents instead.

Option 6: Cloudflare, Sucuri, and other reverse proxies

If you sit behind Cloudflare or a similar CDN, you can strip the header one more layer out, which is nice because it covers every site you route through the same zone without touching the origin. In Cloudflare this is done via a Transform Rule of the type "Modify Response Header":

  • Rule name: "Remove X-Powered-By"
  • When incoming requests match: Hostname equals yourdomain.com (or a wildcard if you want zone wide)
  • Then: Remove header X-Powered-By

Sucuri, BunnyCDN, Fastly and most other reverse proxies offer an equivalent. The exact wording in the dashboard varies, but the feature you are looking for is always "modify response headers" or "edge rules".

Other headers worth checking while you are at it

PHP is not the only thing that tends to be loud about itself in response headers. While you have curl open, scan the headers for any of the following:

  • Server: Apache/2.4.58 (Ubuntu) reveals Apache's patch version and your OS flavor. Suppress via ServerTokens Prod and ServerSignature Off in Apache, or server_tokens off in nginx.
  • X-Powered-CMS, X-Generator, X-Drupal-Cache or similar. Usually set by plugins or CMS specific modules. Remove the same way as X-Powered-By.
  • X-AspNet-Version or X-AspNetMvc-Version. Only relevant if anything in your stack touches .NET, but worth knowing.

InspectWP flags all of these in the security headers section of your report. Once you have the PHP version hidden, running a fresh scan is the quickest way to see which other little giveaways are still on the wire.

Verify and you are done

After whichever route you chose, do one last check:

  1. Open a terminal and run curl -I https://yourdomain.com.
  2. Look through the output. There should be no X-Powered-By line at all.
  3. Repeat for an admin URL (for WordPress, try wp-login.php) and for an error page (append a random string to the URL to trigger a 404). The header should stay gone everywhere.
  4. If you use Cloudflare or a cache, also test from a fresh IP or with cache bypass, so you know you are looking at a live response and not a cached one.
  5. Run a new InspectWP scan. The PHP version check in the Hosting section will now fall back to other detection methods or mark the version as not disclosed, which is what you want.

Hiding the PHP version is not a substitute for keeping PHP up to date. It is one small step in a bigger hardening picture. But it is the kind of quick win that takes five minutes, removes one free piece of information that attackers otherwise collect for nothing, and costs you exactly zero in return.

Check your WordPress site now

InspectWP analyzes your WordPress site for security issues, SEO problems, GDPR compliance, and performance — for free.

Analyze your site free