Subject: Re: [TLS] Connection diversion to other subdomains

From: Florian Weimer <[email-masked]>
Date: Fri, 05 Nov 2010 10:25:34 +0000

* Matt McCutchen:

> I looked through my browser history for registered domains containing a
> TLS web server bearing a wildcard certificate and another subdomain on a
> different IP address.  In almost all cases, the server was willing to
> send me wrong content when I asked for the other subdomain via SNI and
> HTTP Host.  Strictly speaking, this is a vulnerability.  Usually the
> effect is just goofy, though on one major web site which I won't name,
> it led to XSS.

How is this even possible given that browsers do not send such crafted
requests?

-- 
Florian Weimer                <[email-masked]>
BFK edv-consulting GmbH       http://www.bfk.de/
Kriegsstraße 100              tel: +49-721-96201-1
D-76133 Karlsruhe             fax: +49-721-96201-99

---

From: Marsh Ray <[email-masked]>
Date: Fri, 05 Nov 2010 11:07:09 -0500

> How is this even possible given that browsers do not send such crafted

DNS poisoning or any other means of TCP redirection.

- Marsh

---

From: Florian Weimer <[email-masked]>
Date: Fri, 05 Nov 2010 16:14:48 +0000

> DNS poisoning or any other means of TCP redirection.

Ah, I misread Matt's comment, I thought he was referring to Host (SNI)
/= Host (HTTP header).

Yes, obviously, your wildcard certificate is more trusted than your
host-specific certificate, and this has to be reflected in the server
setup etc.  I can see that reality is quite different from that.  (The
problem that your ad servers are the most trusted node on your network
is a similar phenomenon.)

---

From: Marsh Ray <[email-masked]>
Date: Fri, 05 Nov 2010 12:00:46 -0500

> Ah, I misread Matt's comment, I thought he was referring to Host (SNI)

It may be that the bad guy is, in fact, the one crafting the request. 
Hopefully, this is an uninteresting scenario because the bad guy doesn't 
have legitimate credentials and thus it's "safe" to let him connect 
anywhere he likes.

On the other hand, some little script injected into an authenticated 
session could be making XMLHttpRequests. It looks like W3C says the 
browser is supposed to prevent him from setting _that_ particular 
header. As long as all the browsers implement the W3C recommendations 
perfectly, we're safe, right?
http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method

Of course the problem with blacklist-based security (as opposed to a 
whitelist) is that any omission defaults to being a silent security hole 
(as opposed to a bug in functionality that gets fixed before release).

W3C obviously couldn't track down all possible name variations on every 
security-relevant HTTP header that might be interpreted by every server 
everywhere.

How about the good old "X-" header prefix? That's a common variation.

Web search for "X-host" turns up:

http://blog.preinheimer.com/index.php?/archives/349-X-Host-IP.html
"We’re planning to roll out a new feature allowing customers to inject a 
hosts file[1] onto the proxy server of their choice to allow server 
selection through a web interface. This should be completed in the next 
few weeks."    Hmmm, what could possibly go wrong?

More worryingly,
http://www.alphastate.com/x-host-vs-host-headers-in-apache   says:
"Apache picks the X-Host header over the Host header, so if there are 
two different virtual hosts for origin.example.com and www.example.com 
the request ends up at www.example.com instead of the intended 
origin.example.com."

I haven't actually tested this myself, but it would appear that there 
are probably some circumstances where ordinary client-side script (XSS 
injection) can result in a request coming from an attacker-crafted Host 
(or equivalent) header which differs from what the browser sends in the SNI.

Another scenario to consider involves an external SSL offload device. 
Such a front-end device may see the SNI, yet have no way to convey it to 
the server, other than perhaps injecting an X-Host-like header of his 
own. The device may do some degree of sanitization on the 
client-supplied headers, or it may not. The potential exists for an 
attacker to supply conflicting values for some of Host, X-Host, 
X-TLS-SNI to some other target web server IP, possibly from within an 
authenticated browser session.

> is a similar phenomenon.)

The phrase "house of cards" comes to mind here.

- Marsh

---

From: Joe Orton <[email-masked]>
Date: Fri, 5 Nov 2010 14:04:35 -0400

On Fri, Nov 05, 2010 at 12:00:46PM -0500, Marsh Ray wrote:
> "Apache picks the X-Host header over the Host header, so if there
> are two different virtual hosts for origin.example.com and
> www.example.com the request ends up at www.example.com instead of
> the intended origin.example.com."

That's complete fiction, Apache httpd does no such thing.

Regards, Joe

---

From: Marsh Ray <[email-masked]>
Date: Fri, 05 Nov 2010 15:15:16 -0500

> That's complete fiction, Apache httpd does no such thing.

That's good to know.

If nothing else though, it suggests there are app developers out there 
thinking about using X-Host and other random headers for this and that.

I tried this grep in the Apache 2.2 source. (Sorry I can't predict how 
Thunderbird will wrap it.)

These results should give a crude approximation of client-supplied 
headers which are referenced by Apache source which are not included in 
the blacklist recommended by W3C.

> marsh@lamb:~/tmp/httpd-2.2.17$ grep -R headers_in modules/ | grep '"' | egrep -vi
'"accept-charset"|"accept-encoding"|"connection"|"content-length"|"cookie"|"cookie2"|"content-transfer-encoding"
|"date"|"expect"|"host"|"keep-alive"|"referer"|"te"|"trailer"|"transfer-encoding"|"upgrade"|"user-agent"|"via"'

> modules/dav/main/mod_dav.c:    const char *depth = apr_table_get(r->headers_in, "Depth");
> modules/dav/main/mod_dav.c:    const char *overwrite = apr_table_get(r->headers_in, "Overwrite");
> modules/dav/main/mod_dav.c:        label = apr_table_get(r->headers_in, "label");
> modules/dav/main/mod_dav.c:    range_c = apr_table_get(r->headers_in, "content-range");
> modules/dav/main/mod_dav.c:    dest = apr_table_get(r->headers_in, "Destination");
> modules/dav/main/mod_dav.c:        const char *nscp_path = apr_table_get(r->headers_in, "New-uri");
> modules/dav/main/mod_dav.c:            if (apr_table_get(r->headers_in, "label") != NULL) {
> modules/dav/main/mod_dav.c:    dest = apr_table_get(r->headers_in, "Destination");
> modules/dav/main/util.c:    const char *timeout_const = apr_table_get(r->headers_in, "Timeout");
> modules/dav/main/util.c:    if ((str = apr_pstrdup(r->pool, apr_table_get(r->headers_in, "If"))) == NULL)
> modules/dav/main/util.c:    if ((if_match = apr_table_get(r->headers_in, "If-Match")) != NULL) {
> modules/dav/main/util.c:            apr_table_get(r->headers_in, "If-None-Match")) != NULL) {
> modules/proxy/mod_proxy_ftp.c:    if ((password = apr_table_get(r->headers_in, "Authorization")) != NULL
> modules/proxy/mod_proxy_http.c:        apr_table_mergen(r->headers_in, "X-Forwarded-For",
> modules/proxy/mod_proxy_http.c:            apr_table_mergen(r->headers_in, "X-Forwarded-Host", buf);
> modules/proxy/mod_proxy_http.c:        apr_table_mergen(r->headers_in, "X-Forwarded-Server",
> modules/proxy/mod_proxy_http.c:        if (!strcasecmp(headers_in[counter].key,"Proxy-Authorization")) {
> modules/proxy/mod_proxy_http.c:            if (    !strcasecmp(headers_in[counter].key, "If-Match")
> modules/proxy/mod_proxy_http.c:                 || !strcasecmp(headers_in[counter].key, "If-Modified-Since")
> modules/proxy/mod_proxy_http.c:                 || !strcasecmp(headers_in[counter].key, "If-Range")
> modules/proxy/mod_proxy_http.c:                 || !strcasecmp(headers_in[counter].key, "If-Unmodified-Since")
> modules/proxy/mod_proxy_http.c:                 || !strcasecmp(headers_in[counter].key, "If-None-Match")) {
> modules/proxy/mod_proxy_http.c:        buf = apr_pstrcat(p, headers_in[counter].key, ": ",
> modules/proxy/proxy_util.c:                proxy_auth = apr_table_get(r->headers_in, "Proxy-Authorization");
> modules/proxy/mod_proxy.c:    if ((str = apr_table_get(r->headers_in, "Max-Forwards"))) {
> modules/proxy/mod_proxy.c:        apr_table_set(r->headers_in, "Max-Forwards",
> modules/filters/mod_deflate.c:        if (apr_table_get(r->headers_in, "Content-Range") != NULL) {
> modules/filters/mod_deflate.c:        apr_table_unset(r->headers_in, "Content-MD5");
> modules/filters/mod_ext_filter.c:            ctypes = apr_table_get(f->r->headers_in, "Content-Type");
> modules/filters/mod_filter.c:                apr_table_setn(r->headers_in, "Range", rctx->range);
> modules/filters/mod_filter.c:            ctx->range = apr_table_get(r->headers_in, "Range");
> modules/filters/mod_filter.c:            apr_table_unset(r->headers_in, "Range");
> modules/aaa/mod_authnz_ldap.c:    char *lang_line = (char*)apr_table_get(r->headers_in, "accept-language");
> modules/mappers/mod_negotiation.c:    const char *negotiate = apr_table_get(r->headers_in, "Negotiate");
> modules/http/byterange_filter.c:    return (apr_table_get(r->headers_in, "Request-Range")
> modules/http/byterange_filter.c:    if (!(range = apr_table_get(r->headers_in, "Range"))) {
> modules/http/byterange_filter.c:        range = apr_table_get(r->headers_in, "Request-Range");
> modules/http/byterange_filter.c:    if ((if_range = apr_table_get(r->headers_in, "If-Range"))) {
> modules/http/http_protocol.c:    if ((if_match = apr_table_get(r->headers_in, "If-Match")) != NULL) {
> modules/http/http_protocol.c:        if_unmodified = apr_table_get(r->headers_in, "If-Unmodified-Since");
> modules/http/http_protocol.c:    if_nonematch = apr_table_get(r->headers_in, "If-None-Match");
> modules/http/http_protocol.c:                if (apr_table_get(r->headers_in, "Range")) {
> modules/arch/netware/mod_nw_ssl.c:                result = apr_table_get(r->headers_in, "Forwarded");
> modules/arch/netware/mod_nw_ssl.c:                result = apr_table_get(r->headers_in, "Proxy-Connection");
> modules/arch/netware/mod_nw_ssl.c:                result = apr_table_get(r->headers_in, "Accept");
> modules/ssl/ssl_engine_kernel.c:    if ((auth_line = apr_table_get(r->headers_in, "Authorization"))) {
> modules/ssl/ssl_engine_kernel.c:    apr_table_set(r->headers_in, "Authorization", auth_line);
> modules/ssl/ssl_engine_vars.c:                result = apr_table_get(r->headers_in, "Forwarded");
> modules/ssl/ssl_engine_vars.c:                result = apr_table_get(r->headers_in, "Proxy-Connection");
> modules/ssl/ssl_engine_vars.c:                result = apr_table_get(r->headers_in, "Accept");
> modules/cache/mod_cache.c:    auth = apr_table_get(r->headers_in, "Authorization");
> modules/cache/mod_cache.c:    else if (apr_table_get(r->headers_in, "Authorization") != NULL
> modules/cache/cache_storage.c:                apr_table_unset(r->headers_in, "If-Match");
> modules/cache/cache_storage.c:                apr_table_unset(r->headers_in, "If-Modified-Since");
> modules/cache/cache_storage.c:                apr_table_unset(r->headers_in, "If-None-Match");
> modules/cache/cache_storage.c:                apr_table_unset(r->headers_in, "If-Range");
> modules/cache/cache_storage.c:                apr_table_unset(r->headers_in, "If-Unmodified-Since");
> modules/cache/cache_storage.c:                apr_table_unset(r->headers_in, "Range");
> modules/cache/cache_storage.c:                        apr_table_set(r->headers_in, "If-None-Match", etag);
> modules/cache/cache_storage.c:                        apr_table_set(r->headers_in, "If-Modified-Since",
> modules/cache/cache_util.c:    cc_req = apr_table_get(r->headers_in, "Cache-Control");
> modules/cache/cache_util.c:    pragma = apr_table_get(r->headers_in, "Pragma");

Most of these probably do not represent a problem. But some of them look 
a bit suspicious.

For example, this code from mod_proxy_http.c:
>         /* Add X-Forwarded-Host: so that upstream knows what the
>          * original request hostname was.
>          */
>         if ((buf = apr_table_get(r->headers_in, "Host"))) {
>             apr_table_mergen(r->headers_in, "X-Forwarded-Host", buf);
>         }

(The function apr_table_mergen looks like it appends the value as a 
comma-separated list if the key already exists).

If there is actually an "upstream" of a mod_proxy_http server which 
depends on "knowing what the original request hostname was", he could 
end up receiving something like:

    X-Forwarded-Host: <attacker-supplied>,userforums.example.com

So I think there's probably reason to be concerned about this class of bugs.

- Marsh

---

From: Matt McCutchen <[email-masked]>
Date: Wed, 17 Nov 2010 19:47:36 -0500

For anyone following this story:

On Thu, 2010-10-28 at 00:01 -0400, Matt McCutchen wrote:
> [...] though on one major web site which I won't name, it led to XSS.

This was Mozilla.  They have corrected the issue.  Here's the bug:

https://bugzilla.mozilla.org/show_bug.cgi?id=607807

-- 
Matt