---
title: What your server headers reveal: exposed versions, old software and CVEs
description: Every response your site sends can name the web server, PHP, the CMS and the JavaScript libraries behind it, down to the patch release. Here is what attackers read from headers and HTML, how to check your own site, how to hide versions on nginx, Apache, IIS, PHP and WordPress, and why patching matters more than hiding.
canonical: https://watchfor.io/blog/what-your-server-headers-reveal
---

# What your server headers reveal: exposed versions, old software and CVEs

2026-10-22 · WatchFor Team · Guides · guides, security, vulnerabilities, http

Every response your site sends can name the web server, PHP, the CMS and the JavaScript libraries behind it, down to the patch release. Here is what attackers read from headers and HTML, how to check your own site, how to hide versions on nginx, Apache, IIS, PHP and WordPress, and why patching matters more than hiding.

Open your browser's network tab on almost any website and look at the first
response. There is a good chance it names the software that produced it:
`Server: Apache/2.4.29 (Ubuntu)`, `X-Powered-By: PHP/7.4.33`, a
`<meta name="generator" content="WordPress 5.8">` tag, a script loaded from
`/ajax/libs/jquery/1.12.4/`. None of it is a vulnerability by itself. All of
it tells a stranger which known bugs are worth trying first.

The free [Software Exposure Checker](/software-exposure-checker) reads those
signals the way a scanner would, from one ordinary GET of your page, and
grades what it finds A+ to F: what you reveal, what is outdated or past end
of life, and which known CVEs may apply. This guide explains what it looks
at, how to read the result, and how to make your site say less.

## What a single response gives away

A scanner does not need to log in or guess paths. One request is enough to
build a short list of the stack:

| Where | Example | What it tells a stranger |
| --- | --- | --- |
| `Server` header | `Apache/2.4.29 (Ubuntu) OpenSSL/1.1.1f` | Web server, TLS library and operating system, each with a version |
| `X-Powered-By` | `PHP/7.4.33`, `ASP.NET`, `Express` | The language runtime or framework |
| `X-AspNet-Version` | `4.0.30319` | The .NET runtime behind an IIS site |
| `X-Generator` | `Drupal 7 (http://drupal.org)` | The CMS and its major version |
| Meta generator tag | `WordPress 5.8` | The CMS release, to the patch level |
| Asset URLs | `wp-emoji-release.min.js?ver=5.8` | The same WordPress version, even with the tag removed |
| Script URLs | `jquery/1.12.4/jquery.min.js` | Front-end libraries and their exact releases |
| Error pages | `<center>nginx/1.18.0 (Ubuntu)</center>` | The server version, on every 404 and 502 |

Put together, that is a map: which server to target, which PHP bugs to try,
which WordPress release to look up. Scanners that sweep the internet for a
freshly published CVE start exactly there, because a version number is the
cheapest way to skip the servers that are already patched.

## Check your own site

In the [Software Exposure Checker](/software-exposure-checker), enter your
site and run the check. A probe near you fetches the page once, follows
redirects and reads the response headers and the first 256 KB of HTML. It
never requests script files, guesses paths or sends anything exploit-like.

Every signal is listed in a **What you reveal** table with the exact header
line or HTML snippet, the product and version it names, and how sure we are:

- **exact** — the software states its version (`Server: nginx/1.25.3`);
- **distro build** — a Debian, Ubuntu, Red Hat or SUSE package, whose
  number may hide backported fixes (more on that below);
- **inferred** — read from something not meant as a version statement, such
  as WordPress's `?ver=` on its own core files.

Each component is then placed against its release calendar and known CVEs:
**current**, **outdated** (behind the latest patch release of its branch),
**EOL** (past end of life) or **verify** (a distribution build whose number
falls in a vulnerable range).

### A worked example

The checker's own demo page, `https://watchfor.io/demo/exposure/outdated`,
answers like an old WordPress site. One GET gives away five versions:

- `X-Powered-By: PHP/7.4.33` — PHP 7.4 reached end of life on Nov 28, 2022
  and got no fix for php2shell. This alone sets the grade to **F**, the same
  verdict the [PHP Version Checker](/php-version-checker) gives.
- `Apache/2.4.29 (Ubuntu)` in a server signature — 40 releases behind 2.4.69
  by number, and in the range of several known-exploited CVEs. It is an
  Ubuntu package, so the checker says **verify**, not "affected": Ubuntu may
  have backported the fixes.
- `<meta name="generator" content="WordPress 5.8">` and a core asset with
  `?ver=5.8` — behind 5.8.18, the latest security release of that branch.
- `jquery/1.12.4/jquery.min.js` — jQuery 1.x has been out of support since
  2016, and CVE-2020-11023, which affects it, is in CISA's Known Exploited
  Vulnerabilities catalog.
- Bootstrap 3.3.7 from its CDN — end of life since Jul 24, 2019.

The result leads with the grade and the first thing to fix, then lists every
finding worst first, each with the exact config change or upgrade.

## Why "may be affected", and why distribution builds say "verify"

A version number is a lead, not proof. Two cases matter:

**Backports.** Debian, Ubuntu, Red Hat and SUSE fix security bugs inside their
own packages without changing the upstream version. Ubuntu 18.04's Apache says
`2.4.29` for its whole life, patched or not. So a distribution build is never
failed on its number alone: the checker caps it at **C**, marks it
**verify** and links the vendor's CVE tracker. Debian's nginx sends no OS
token at all, so a plain `nginx/1.22.1` — the exact version Debian 12 ships —
is treated as a likely Debian package too. Then you check the package
changelog:

```bash
# Debian / Ubuntu: is the fix in the installed package?
apt changelog apache2 | grep -E 'CVE-2021-40438|CVE-2024-38475'
# RHEL family
rpm -q --changelog httpd | grep -E 'CVE-2021-40438|CVE-2024-38475'
```

**Front-end libraries.** A jQuery CVE only matters if the page passes
untrusted input to the affected function. The checker can't know how your
code uses the library, so a front-end library with a high, critical or
known-exploited CVE, or past end of life, caps at **C** and says so. Upgrading
is still the right move: it is usually cheaper than proving you are safe.

## Hide the versions

Once the software is current, stop announcing it. Per server:

**nginx** — hides the version in `Server` and on its error pages:

```nginx
server_tokens off;
```

**Apache** — `Prod` sends just `Apache`; `ServerSignature Off` removes the
footer from error pages and directory listings (on Debian and Ubuntu, both
live in `conf-available/security.conf`):

```apache
ServerTokens Prod
ServerSignature Off
Header always unset X-Powered-By
```

**IIS** — IIS 10 can drop the `Server` header itself; ASP.NET's version header
is a separate switch:

```xml
<system.webServer>
  <security>
    <requestFiltering removeServerHeader="true" />
  </security>
  <httpProtocol>
    <customHeaders>
      <remove name="X-Powered-By" />
    </customHeaders>
  </httpProtocol>
</system.webServer>
<system.web>
  <httpRuntime enableVersionHeader="false" />
</system.web>
```

**PHP** — stops `X-Powered-By: PHP/x.y.z`:

```ini
; php.ini
expose_php = Off
```

**WordPress** — removes the generator tag (and the version from RSS feeds);
the `?ver=` on core assets needs a filter of its own, which also drops
cache-busting for those files on the next update:

```php
// functions.php of your child theme, or a small plugin
remove_action( 'wp_head', 'wp_generator' );
add_filter( 'the_generator', '__return_empty_string' );
```

**Express and Next.js**:

```js
app.disable("x-powered-by");                       // Express
module.exports = { poweredByHeader: false };       // next.config.js
```

**Cloudflare** — when you can't change the origin, a Transform Rule removes a
header at the edge: *Rules → Transform Rules → Modify Response Header →
Remove*, header name `X-Powered-By`. (Cloudflare already replaces `Server`
with its own.)

Then run the [Software Exposure Checker](/software-exposure-checker) again.
It should find nothing to name; that is an **A+**, with the honest note that
we can only see what the page shows.

## Patching beats hiding

Hiding a version is good practice, and it is security through obscurity in
the plain sense of the phrase: it changes what a stranger can see, not what
runs. Attackers fire exploits blind when a bug is worth it, and mass scans
for a new CVE often skip fingerprinting entirely. A hidden PHP 7.4 is still
PHP 7.4.

So the order is:

1. **Patch.** Upgrade what is outdated, move what is past end of life to a
   supported branch, and confirm distribution packages carry the fixes.
2. **Restart.** A new package on disk changes nothing until PHP-FPM, Apache
   or nginx is restarted.
3. **Hide.** Turn the versions off so the next scanner learns less.
4. **Check again** from outside, because a reverse proxy, a CDN rule or a
   forgotten server can still answer with the old headers.

## Keep watching

Version disclosure tends to come back: a package upgrade resets a config
file, a new server joins the pool, a plugin re-adds its generator tag.

- An [API monitor](/api-monitoring) on your home page can assert on headers:
  `X-Powered-By` **does not exist**, and `Server` **matches regex**
  `^[^0-9]*$` (no digits, so no version). It alerts the day disclosure
  returns.
- [Page integrity monitoring](/page-integrity-monitoring) alerts when a page
  starts loading a script from a new source — an old library swapped back in,
  or something worse.

And for a quick look at any site you run, the
[Software Exposure Checker](/software-exposure-checker) answers in a few
seconds — or from a terminal: `curl https://watchfor.io/exposure/example.com`.
For PHP specifically, the [PHP Version Checker](/php-version-checker) adds the
branch calendar, the patch gap and the exact release to install.

---

Canonical page: https://watchfor.io/blog/what-your-server-headers-reveal · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
