Title Background Image

The Host Header Injection Booboo


How to detect this vulnerability and how to protect against it.

One of the more unpleasant parts of being a site operator are these obscure vulnerabilities showing up here and there. One can misconfigure some parts of the server, often with the best intentions on something else. Here I won’t elaborate much on how evil HTTP Host Header Injection can be, but show how to detect the vulnerability and, if it’s there, on how to fix it.

By Markus “Traumflug” Hitter

Earlier this week I was asked for advice on two vulnerabilities somebody assumed to exist on his site. Lo’ and behold, s/he was right both times. One was reported on the thirty bees bug tracker, for the other one I decided to write this post, because it can’t always be fixed on the thirty bees side.

The Vulnerability

Others wrote a lot more about this, no need to duplicate that. For example here, here and here.

In short, sending forged requests to misconfigured servers can cause these servers to redirect browsers to arbitrary domains outside the own realm. Such answers can happen to be be cached (cache poisoning, think Cloudflare), redirecting legitimate users as well.

On a different level, vulnerable PHP code can do bad decisions based on requests forged the same way.

“Forged” means, HTTP requests with a malicious Host or X-Forwarded-Host header.

Is My Server Vulnerable?

Good thing is, detection of such a vulnerability on the server level is quite easy. Tools like Curl allow to send forged requests easily, so one can simply try.

Testing the Server

A server should never redirect a request to a domain it isn’t intended to handle. Does it nevertheless, its configuration needs care. Finding out is as simple as looking at the response to a request with a forged HTTP header.

Detection Code

Unintended redirection can be tested with a simple command:

curl -v -H "Host: evil.com" https://<my host> 2>&1 | grep evil.com

Let’s demonstrate on a vulnerable server:

~$ curl -v -H "Host: evil.com" https://<my host> 2>&1 | grep evil.com
> Host: evil.com
< Location: https://evil.com/cgi-sys/suspendedpage.cgi

(Lines starting with > show data sent to the server, lines starting with < show responses.)

Looking at the full response (remove | grep evil.com), this Location header is part of a HTTP Redirect. Which means this request redirects to outside your realm, to the attacker’s site. Usually not what one wants.

For a not vulnerable site, output of the same command looks like this:

~$ curl -v -H "Host: evil.com" https://<my host> 2>&1 | grep evil.com
> Host: evil.com

As one can see, the forged header gets sent as well, but is found nowhere in the response. Safe!

What to Test

One has to test all hosts one serves, with HTTP as well as with HTTPS. And remember, example.com and www.example.com are two distinct hosts, one has to test both.

Yet more, test all of them with two distinct forged headers, Host and X-Forwarded-Host.

To make things a bit easier, here’s a code snippet. Type the first line into a shell, adjust it to the hosts you want to test, separated by space. Then Copy & Paste the remaining part.

HOSTS="example.com www.example.com"

for H in ${HOSTS}; do
  curl -Isk -H "Host: evil.com" http://${H}/ | grep -q evil.com \
  && echo "${H} vulnerable via HTTP with header 'Host'."
  curl -Isk -H "X-Forwarded-Host: evil.com" http://${H}/ | grep -q evil.com \
  && echo "${H} vulnerable via HTTP with header 'X-Forwarded-Host'."
  curl -Isk -H "Host: evil.com" https://${H}/ | grep -q evil.com \
  && echo "${H} vulnerable via HTTPS with header 'Host'."
  curl -Isk -H "X-Forwarded-Host: evil.com" https://${H}/ | grep -q evil.com \
  && echo "${H} vulnerable via HTTPS with header 'X-Forwarded-Host'."
done

If this creates output not starting with Testing, this host needs fixing. Use code snippets shown above for more details.

Testing PHP

Well, there’s not much to test, forged requests shouldn’t even be allowed to start executing PHP code. Also, behavior of PHP with forged HTTP headers is well known and documented. For completeness, let’s demonstrate this.

To allow testing PHP, a small helper file is needed on the server. Usually with a .php suffix to have it processed by PHP. Content of this file:

<?php phpinfo(); ?>

Given a test doesn’t fail on the server level like above already, one can easily see PHP’s vulnerabilities (the sed command removes uninteresting parts):

~$ curl -H "Host: evil.com" http://<host>/phpinfo.php 2>&1 | \
      sed -n '/$_SERVER.*evil.com/ { s/<[^>]*>/ /g; p }'
  $_SERVER['HTTP_HOST']  evil.com
  $_SERVER['SERVER_SIGNATURE']  &lt;address&gt;Apache/2.4.41 (Ubuntu) Server at evil.com Port 80&lt;/address&gt;
  $_SERVER['SERVER_NAME']  evil.com

Conclusion: fields HTTP_HOST, SERVER_SIGNATURE and SERVER_NAME in $_SERVER[] can’t be trusted.

Don’t forget to remove this helper file after being done with these tests.

Fixing

Such vulnerabilities should be fixed, of course. As far as I can tell, there are currently not many exploits, but perhaps some evil troll tries his luck before too long anyways.

Fixing the Server

Server redirects can’t be fixed in thirty bees, nor by PHP in general. This has to be done in the server configuration.

General rule: redirect (or rebase) only to hosts you know about. Either by using hardcoded redirects or by evaluating server variables before using them.

Examples shown here avoid redirection to outside the own domain, which is sufficient to make header injection pointless for attackers. They’d still allow redirection inside the own domain, which is usually harmless or even wanted.

Nginx

These examples are taken from a configuration section redirecting HTTP traffic to HTTPS.

Bad, note the unconditional usage of $host:

server {
  server_name .example.com;
  [...]
  return 301 https://$host$request_uri;
}

Good, note the usage of $server_name (name of the server accepting the request) rather than $host (content of the ‘Host’ header):

server {
  server_name .example.com;
  [...]
  return 301 https://$server_name$request_uri;
}

Good, note the redirection only to known domains:

server {
  server_name .example.com;
  [...]
  if ($host ~ example.com$) {
    # Legitimate requests.
    return 301 https://$host$request_uri;
  }
  # Forged requests.
  return 444;
}

Apache

TODO, have to find a server running Apache for testing. For the time being, a blog post by Lokesh Sharma gives good hints.

LiteSpeed

TODO, have to find a server running LiteSpeed.

Fixing PHP

Some thirty bees methods and some of the vendor dependencies (e.g. Guzzle) use PHP $_SERVER['HTTP_HOST'] or $_SERVER['SERVER_NAME'] in unsafe ways. This makes it even more important to catch forged requests on the server level already. Requests rejected or redirected there don’t hit PHP, so they can’t do any harm.