Skip to content

Host header injection

CWE-644CWE-601OWASP A03:2021Updated September 4, 20265 min read

Many applications derive their own address from the Host header of the incoming request. That header is set by the client, so an attacker can replace it. If that value ends up in a password reset link, the recovery token is sent to the attacker's server the moment the victim clicks it.

An application sometimes needs to know its own address: to put a link in an email, to build an absolute reference, to decide which customer is being served. The simplest source for that seems to be the Host header of the incoming request. Only that source is not yours; it belongs to whoever sends the request. Where that leads is rarely harmless.

What is host header injection?

Every HTTP request contains a Host header naming the server being addressed. That header is needed because one IP address can serve many domains; it tells the web server which site is meant. We speak of host header injection when an application takes that value into its own output without checking that it is one of its own addresses.

The mistake is subtle but fundamental. The Host header feels like infrastructure, something belonging to the server rather than the user. In reality it simply sits in the request and anyone composing their own requests can put in whatever they want. It is therefore input, and it deserves the same suspicion as a form field.

Picture a reply envelope on which the sender fills in the return address themselves. The form arrives with you neatly and you send the answer back to the address given. That the answer thereby lands somewhere other than with the rightful owner only becomes apparent when it is too late.

How does a host header injection attack work?

The most concrete abuse sits in the password reset function, because that is where a secret is sent by email.

Vulnerable:

app.post('/forgot-password', async (req, res) => {
  const user = await users.findByEmail(req.body.email);
  if (user) {
    const token = await createResetToken(user);

    // Base URL from the request: under the attacker's control
    const link = `https://${req.headers.host}/reset?token=${token}`;
    await mail.send(user.email, `Reset your password: ${link}`);
  }
  res.send('If this address is known, you will receive an email.');
});

The attacker requests a reset for the victim’s address, but alters the header:

POST /forgot-password HTTP/1.1
Host: malicious.example
Content-Type: application/json

{"email":"j.walker@company.com"}

The victim receives an email genuinely from your organisation, with the right salutation and the right subject. Only the link points at https://malicious.example/reset?token=.... If they click it, the attacker’s server receives the valid reset token, with which the attacker can then set the password themselves. The victim saw nothing suspicious: after all, they had asked for a reset, or so they thought.

Safe:

// The application knows the address it runs at
const BASE_URL = process.env.BASE_URL;             // https://portal.example

app.post('/forgot-password', async (req, res) => {
  const user = await users.findByEmail(req.body.email);
  if (user) {
    const token = await createResetToken(user);
    const link = `${BASE_URL}/reset?token=${token}`;
    await mail.send(user.email, `Reset your password: ${link}`);
  }
  res.send('If this address is known, you will receive an email.');
});

// Safety net: refuse requests with an unknown hostname
const ALLOWED_HOSTS = new Set(['portal.example', 'www.portal.example']);

app.use((req, res, next) => {
  const host = req.headers.host?.split(':')[0];
  if (!ALLOWED_HOSTS.has(host)) return res.status(400).send('Invalid host');
  next();
});

The base URL now comes from configuration and is therefore a property of the installation rather than of the request. The additional check ensures requests with an unknown hostname are not processed at all, which also catches the variants you had not anticipated. If you serve multiple domains, determine the base URL from a validated value on that list.

Do not forget X-Forwarded-Host. Many frameworks use that header when determining the base URL if the application runs behind a proxy. If it is accepted from any client rather than only from your own trusted proxy, protection on the Host header is easily bypassed.

What is the impact of host header injection?

The severity runs from medium to high, with the password reset route weighing heaviest. There the vulnerability leads to a full account takeover without the attacker needing to know anything about the victim beyond their email address.

What makes the attack effective is its credibility. The email genuinely comes from you: it is sent by your server, comes from your domain, passes your SPF and DKIM checks and looks exactly as your users expect. There is no signal that would warn an attentive recipient, apart from the link itself, and that is rarely checked in an email one is expecting.

Beyond email there are further consequences. If the response is cached, a poisoned version with links to the attacker’s domain can be served to other visitors. In environments where a proxy routes or authorises based on the hostname, a manipulated header can direct traffic to an internal application. And with applications selecting a per-customer environment based on the subdomain, it can grant access to someone else’s data.

How do you detect host header injection?

A tester sends requests with an altered Host header and checks whether that value reappears anywhere in the response: in a redirect, in an absolute link in the HTML, in a Location header or in a reference to a script file. If the supplied value appears, the header is being taken over somewhere.

The most important test is on the password reset function, because the effect is clearest there: request a reset with a deviating host and check which link ends up in the email. Testers further try whether the check can be bypassed with X-Forwarded-Host, with a port number after the hostname, with a duplicate Host header or with an absolute URL on the request line. Caching behaviour is examined too: is a response with a strange host stored and served to others? AssistSec covers the full chain including the proxy or load balancer in front, because the question of which component may determine the hostname is answered there.

How do you prevent host header injection?

  • Take the base URL from your configuration and never from the Host header of the request.
  • Refuse requests whose hostname is not on your list of permitted domains.
  • Accept X-Forwarded-Host and similar headers only from your own trusted proxy.
  • Build links in emails always with the configured value, including in background processes.
  • Configure your web server with an explicit default host that rejects unknown names.
  • Keep the hostname out of cached responses, or let the cache vary on that value.
  • Select a customer environment based on a validated value rather than the raw header.
  • Check on a password reset that the token is only usable on your own domain.

Sources

Frequently asked questions

Why can the Host header not be trusted?

Because it is part of the request and therefore set entirely by the sender. A browser fills it in correctly, but an attacker composing their own requests puts in whatever they like. Anything the server derives from it is thereby under the attacker's influence.

What are X-Forwarded-Host and similar headers?

Headers a proxy adds to pass on the original hostname. Many frameworks take them into account when determining the base URL. If they come straight from a client, they are just as untrustworthy as the Host header, and they are more often overlooked.

How should I set the base URL instead?

As a fixed value in your configuration, per environment. The application should know the address it is reachable at; that is a property of the installation and not something varying per request. With multiple domains, keep a list and validate against it.

Is this a risk without email?

Yes. The header can also end up in the cache, so a poisoned page with links to the attacker's domain gets served to other visitors. It can further be abused to influence routing decisions or access controls in a proxy.

Related articles

Press / to search · Esc