# X-Powered-By Header: Remove It in Express, PHP, Next.js

> X-Powered-By advertises your framework, such as Express or PHP. Remove it in Express, PHP, Next.js, ASP.NET and nginx, and why it is hygiene, not security.

Source: https://howhttpworks.com/headers/x-powered-by
Last reviewed: 2026-10-04

> **TL;DR:** `X-Powered-By` is a non-standard header that frameworks add to name themselves (`Express`, `PHP/8.3.12`, `ASP.NET`). Turn it off in the framework: `app.disable('x-powered-by')` in Express, `expose_php = Off` in PHP, `poweredByHeader: false` in Next.js. It is hygiene, not security.

## What it looks like

```http
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
```

```http
HTTP/1.1 200 OK
X-Powered-By: PHP/8.3.12
```

It is not defined by any RFC, browsers do nothing with it, and the value format is whatever the framework chose. It is a sibling of the [Server](https://howhttpworks.com/headers/server) header; `Server` names the web server and `X-Powered-By` names the application layer behind it.

## Remove it

### Express

```javascript
import express from 'express'

const app = express()
app.disable('x-powered-by')
```

Or use Helmet, which removes the header among its defaults:

```javascript
import helmet from 'helmet'

app.use(helmet())
```

The Express security guide shows `app.disable('x-powered-by')`, and says this does not prevent a sophisticated attacker from determining that an app is running Express; it may only discourage a casual exploit.

### PHP

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

`expose_php` defaults to on, which makes PHP send `X-Powered-By: PHP/<version>`. The PHP manual lists it as changeable in `php.ini` only, so `ini_set()` at runtime will not work. Reload PHP-FPM or Apache afterwards.

### Next.js

```javascript
// next.config.js
module.exports = {
  poweredByHeader: false
}
```

Next.js adds `x-powered-by: Next.js` by default and this option opts out.

### ASP.NET and IIS

```xml
<!-- web.config -->
<system.webServer>
  <httpProtocol>
    <customHeaders>
      <remove name="X-Powered-By" />
    </customHeaders>
  </httpProtocol>
</system.webServer>
```

### nginx or Apache in front

When the framework cannot be changed, strip it at the proxy. nginx:

```nginx
location / {
    proxy_pass http://app;
    proxy_hide_header X-Powered-By;
}
```

Apache with `mod_headers`:

```apache
Header always unset X-Powered-By
```

`proxy_hide_header` removes the header from the upstream response before it reaches the client. Setting it at the proxy also covers several backends at once.

## How much it matters

Honest accounting. Leaving the header on gives an attacker a free hint about the stack. Removing it:

- stops the trivial banner grab and clears automated audit findings,
- does nothing about how the framework behaves: cookie names such as `connect.sid` or `PHPSESSID`, default error pages, route shapes and static file paths all still point to the stack,
- does not patch anything.

Do it, because it takes one line, then put your effort into updates, dependency audits and real controls such as [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy) and [X-Content-Type-Options](https://howhttpworks.com/headers/x-content-type-options).

## Verify

```bash
curl -sI https://example.com | grep -i -E '^(x-powered-by|server)'
```

Check a 404 and a 500 response as well. Error handlers and upstream proxies sometimes add their own headers separately from the normal path.

## Related

- [Server](https://howhttpworks.com/headers/server) for `server_tokens off` in nginx and `ServerTokens Prod` in Apache
- [X-Content-Type-Options](https://howhttpworks.com/headers/x-content-type-options), [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy)
