New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The X-Forwarded-Host HTTP header is always trusted and is used in url_for #29893

Open
jdleesmiller opened this Issue Jul 22, 2017 · 10 comments

Comments

Projects
None yet
9 participants
@jdleesmiller

jdleesmiller commented Jul 22, 2017

Background

This has been reported twice on the rails HackerOne program, and the recommendation (from Jeremy) was to open a GitHub issue:

Steps to Reproduce

We're going to simulate a host header attack on a sample application. The sample application is a vanilla rails 5.1.2 application with a home page and one route added, foo, to redirect back to the home page. The code is available here: https://github.com/jdleesmiller/forwarded_host_demo

  1. Visit http://forwarded-host-demo.herokuapp.com/ (it may need some time to boot up)

  2. Open the network tools in your browser (I used Chrome) and tick the option to preserve requests.

  3. Click the 'redirect back to home page' link. You are redirected to the home page.

  4. Copy the corresponding request for /foo as a cURL command from the browser's network tools (right click, Copy, Copy as cURL).

  5. To simulate a host header attack, paste the curl command into a terminal and add -H 'X-Forwarded-Host: evil.com'. For example, for one of my requests:

    curl 'http://forwarded-host-demo.herokuapp.com/welcome/foo' -H 'Accept-Encoding: gzip, deflate, sdch' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36' -H 'Accept: text/html, application/xhtml+xml' -H 'Referer: http://forwarded-host-demo.herokuapp.com/' -H 'Cookie: _forwarded_host_session=RjRsTFduOUg5ZStSUCtlVWtYOWN6cVVsVHNzV1Y5aWNWUlZYVXYvcGZKMVhTSmI3Njg5cURiUFltaTE2cVh0STNreENLbmJLR1pNWlkwWDA0UmhNdERpZzEzOW53aWk2QndkdHhNWkp2L0l2bnFoNlpwdUdNekJUVnBmcHc2SUwwZzdCSDh5RTNFOTdCUTVFRkZoLzFRPT0tLWU1M1k4OVpNZ0lIL0UyUXp0SHN0Rnc9PQ%3D%3D--a85dd504e2416f8cbb387b7bb32184dd9e31337a' -H 'Connection: keep-alive' -H 'Turbolinks-Referrer: http://forwarded-host-demo.herokuapp.com/' -H 'X-Forwarded-Host: evil.com' --compressed
    

Expected Result

User is redirected to the home page:

<html><body>You are being <a href="http://forwarded-host-demo.herokuapp.com/">redirected</a>.</body></html>

Observed Result

User is redirected to the home page on evil.com:

<html><body>You are being <a href="http://evil.com/">redirected</a>.</body></html>

(You can also verify the HTTP Location header for the redirect is set to http://evil.com by passing -i to curl.)

System configuration

Rails version: 5.1.2

Ruby version: 2.4.0p0

Analysis

As I understand it, rails uses the X-Forwarded-Host HTTP header in preference to the Host HTTP header in ActionDispatch::Http::URL to compute request.host. request.host is in turn used in url_for.

Impact

This makes it very easy to create an open redirect in a rails application, in addition to creating many opportunities for reflection of URLs controlled by an attacker, since it affects all of rails's URL helpers through url_for.

To exploit it, the attacker needs to be able to inject the X-Forwarded-Host header, which can be accomplished by cache poisoning.

There is at least one public bug report on HackerOne that demonstrates an attack vector, https://hackerone.com/reports/487.

It seems that GitLab tried to patch this problem in https://gitlab.com/gitlab-org/gitlab-ce/issues/17877 by removing the header in their NGINX layer. However, this caused a number of problems, and they had to remove the patch. According to the issue, they are going to try again with a whitelist of hosts.

Whether the X-Forwarded-Host header is under the user's control does depend on the upstream proxy configuration. On heroku, the steps to reproduce above with my sample app show that it is under user control, so that accounts for a large number of rails applications.

Mitigation

This list of best practices https://github.com/ankane/secure_rails says that you should set

config.action_controller.default_url_options = {host: "www.yoursite.com"}
config.action_controller.asset_host = "www.yoursite.com"

to avoid host injection, but when I tested it, it had no effect (edit: it is a partial fix; see #29893 (comment)). (I tried it on this branch: https://github.com/jdleesmiller/forwarded_host_demo/tree/default-host .) I think the request.host takes precedence over the default setting, which is only used when operating without a request, for example in a background worker. (see #29893 (comment))

Express.js has a trust proxy setting that, among other things, determines whether the app will trust X-Forwarded-Host to set the hostname. I could not find any similar option for rails; rails has good handling of IP spoofing with X-Forwarded-For, but I cannot see any countermeasures against X-Forwarded-Host spoofing.

If the header is not required, removing it appears to solve the problem. One way to do this is with a small piece of Rack middleware in config/application.rb:

    class StripXForwardedHost
      def initialize(app)
        @app = app
      end

      def call(env)
        env.delete('HTTP_X_FORWARDED_HOST')
        @app.call(env)
      end
    end
    config.middleware.use StripXForwardedHost

(Edit: This gem also implements the same approach: https://github.com/pusher/rack-headers_filter .)

This is the approach I'm trialling on my app, and so far it works OK.

Jeremy on HackerOne suggested:

Ideally, we'd validate the host header against an allow-list. This is the approach we take for Action Cable allowed origins.

I'm not at this point what the best approach is, so I've opted to open an issue for discussion.

I hope that's all clear. Let me know if you have any questions.

@ankane

This comment has been minimized.

Show comment
Hide comment
@ankane

ankane Jul 27, 2017

I think a simple solution would be for redirect_to some_path to use the host from config.action_controller.default_url_options if it exists (of course, won't protect apps that don't have it set). redirect_to some_url does not have the same issue.

ankane commented Jul 27, 2017

I think a simple solution would be for redirect_to some_path to use the host from config.action_controller.default_url_options if it exists (of course, won't protect apps that don't have it set). redirect_to some_url does not have the same issue.

@ouabing

This comment has been minimized.

Show comment
Hide comment
@ouabing

ouabing Aug 2, 2017

I think the big problem is, this kind of response will be cached by rails if you enable page cache and use url_for in views.

ouabing commented Aug 2, 2017

I think the big problem is, this kind of response will be cached by rails if you enable page cache and use url_for in views.

@ankane

This comment has been minimized.

Show comment
Hide comment
@ankane

ankane Aug 2, 2017

Looked into it a bit more and my comment above isn't necessary (as redirects won't be cached), so I don't think redirects are an actual issue.

As far as I can tell, the mitigation technique above does work (however, it's not enabled by default).

Without it, you can end up with cache poisoning (as you mention @ouabing). The same thing can be accomplished with the Host header. What maybe makes the X-Forwarded-Host header different is some load balancers (like the ones Heroku runs) are configured to route based on the Host header, making it impossible for an evil host to make it to the Rails app.

ankane commented Aug 2, 2017

Looked into it a bit more and my comment above isn't necessary (as redirects won't be cached), so I don't think redirects are an actual issue.

As far as I can tell, the mitigation technique above does work (however, it's not enabled by default).

Without it, you can end up with cache poisoning (as you mention @ouabing). The same thing can be accomplished with the Host header. What maybe makes the X-Forwarded-Host header different is some load balancers (like the ones Heroku runs) are configured to route based on the Host header, making it impossible for an evil host to make it to the Rails app.

@jdleesmiller

This comment has been minimized.

Show comment
Hide comment
@jdleesmiller

jdleesmiller Aug 2, 2017

Thanks, everyone for your comments.

@ankane Thanks for your input. You are right about root_url. However, the default_url_options workaround still does not seem to prevent attacker-controlled redirects.

For ease of reference, I've created a new heroku app, https://forwarded-host-demo-default.herokuapp.com/, that is running the code from the default-host branch (link to diff) of the demo app, which contains the default_url_options workaround.

It's running the same commit, 02590cd5, as the branch.

$ heroku releases -a forwarded-host-demo-default
=== forwarded-host-demo-default Releases - Current: v5
v5  Deploy 02590cd5                                                                                             -snip-  2017/08/02 19:36:38 +0100 (~ 38s ago)
...

When I go through the steps above to capture a request for /welcome/foo and curl it with the -H 'X-Forwarded-Host: evil.com' option:

$ curl 'https://forwarded-host-demo-default.herokuapp.com/welcome/foo' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Accept: text/html, application/xhtml+xml' -H 'Referer: https://forwarded-host-demo-default.herokuapp.com/' -H 'Cookie: _forwarded_host_session=UDBEQXhHclAvY3phbmx1VWRiZWhKV3FMUU9HcmNwU291SHZyOW9xNlJZblAzSW1acE8xYW4zMlcyU2taMUEvcnRObW1BdkJJeEJsalovOWRrejNkOWE2ZWRVeDUzdVhsVUVzKzRnY1Jad1A3L0xnbHRvZ0dxRDZxMDZtZFlXcGx2VFJxQTZ0ekJNb3E4ZVBRSWovaHVnPT0tLWE3WVlTZEtvRjVXUnNNVDQ1MVhnd0E9PQ%3D%3D--a6835674c0bf2075c786df489bafd0b0191b4880' -H 'Connection: keep-alive' -H 'Turbolinks-Referrer: https://forwarded-host-demo-default.herokuapp.com/' --compressed -H 'X-Forwarded-Host: evil.com'

the response is still

<html><body>You are being <a href="https://evil.com/">redirected</a>.</body></html>

In ankane/secure_rails#4, you also suggested that we try to render root_url, which we can do by curling the root with the evil X-Forwarded-Host header. That does indeed render the default value (set to forwarded-host-demo.herokuapp.com on the branch) for root_url, even though request.host is evil.com:

$ curl 'https://forwarded-host-demo-default.herokuapp.com/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Accept: text/html, application/xhtml+xml' -H 'Referer: https://forwarded-host-demo-default.herokuapp.com/' -H 'Cookie: _forwarded_host_session=UDBEQXhHclAvY3phbmx1VWRiZWhKV3FMUU9HcmNwU291SHZyOW9xNlJZblAzSW1acE8xYW4zMlcyU2taMUEvcnRObW1BdkJJeEJsalovOWRrejNkOWE2ZWRVeDUzdVhsVUVzKzRnY1Jad1A3L0xnbHRvZ0dxRDZxMDZtZFlXcGx2VFJxQTZ0ekJNb3E4ZVBRSWovaHVnPT0tLWE3WVlTZEtvRjVXUnNNVDQ1MVhnd0E9PQ%3D%3D--a6835674c0bf2075c786df489bafd0b0191b4880' -H 'Connection: keep-alive' -H 'Turbolinks-Referrer: https://forwarded-host-demo-default.herokuapp.com/' --compressed -H 'X-Forwarded-Host: evil.com'
<!DOCTYPE html>
...
<pre>
  HTTP_X_FORWARDED_HOST = evil.com
  request.host = evil.com
  root_url = https://forwarded-host-demo.herokuapp.com/
</pre>
<a href="/welcome/foo">redirect back to home page</a>
...

If I make the same request to the app without the default_url_options workaround, the root_url is again on evil.com:

curl 'http://forwarded-host-demo.herokuapp.com/' -H 'If-None-Match: W/"254e9ac6e6fdf70d3db343af2e14544f"' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' -H 'Cache-Control: max-age=0' -H 'Cookie: _forwarded_host_session=MHNLQ3IzRUU2bXpyNEhFdSt5M3M4MzV3VkpPTnF2emJ6NFJWeFlJYXB2dmdlQ1IzeHNsMXpqUXJNdHFwWndFZ05hcmxvZloxRi8vejdnYzNEclNrK1VGdnVzZGpVM0RnMFFkUVdsL1V5cHh0akpSQkNXN0xWUURWYU5GRVFrWVhZVVZGZWdoVkxaSE5iclIvY29DK29BPT0tLUhhSlpFQk53clFDL0FrQnNpZW9zRnc9PQ%3D%3D--90e765814b25d1b640f35dce1d4ed9b15716ac09' -H 'Connection: keep-alive' --compressed  -H 'X-Forwarded-Host: evil.com'
<!DOCTYPE html>
...
<pre>
  HTTP_X_FORWARDED_HOST = evil.com
  request.host = evil.com
  root_url = http://evil.com/
</pre>
<a href="/welcome/foo">redirect back to home page</a>
...

So, to sum up, I think this shows that:

  1. In the default configuration, both root_url and request.host can be controlled by the attacker. (Edit: As pointed out above, the bad root_urls used in views can be cached in the rails page cache, which seems like the most serious problem.)

  2. The default_url_options workaround brings root_url back under the application's control, but request.host remains under the attacker's control, which leaves possible open redirects. (Edit: Rails page caching won't cache these redirects, but downstream caches might, which was the issue mentioned in https://hackerone.com/reports/487 . I'd therefore say this workaround is a good one, but it is not a complete workaround.)

Finally, I'd also note that the other workaround I suggested, which was to strip the X-Forwarded-Host header, is also implemented in this gem: https://github.com/pusher/rack-headers_filter --- they also seem to have had this problem and found the same solution (strip the headers).

I'll also edit my comment above to make note of these points. Thanks again!

jdleesmiller commented Aug 2, 2017

Thanks, everyone for your comments.

@ankane Thanks for your input. You are right about root_url. However, the default_url_options workaround still does not seem to prevent attacker-controlled redirects.

For ease of reference, I've created a new heroku app, https://forwarded-host-demo-default.herokuapp.com/, that is running the code from the default-host branch (link to diff) of the demo app, which contains the default_url_options workaround.

It's running the same commit, 02590cd5, as the branch.

$ heroku releases -a forwarded-host-demo-default
=== forwarded-host-demo-default Releases - Current: v5
v5  Deploy 02590cd5                                                                                             -snip-  2017/08/02 19:36:38 +0100 (~ 38s ago)
...

When I go through the steps above to capture a request for /welcome/foo and curl it with the -H 'X-Forwarded-Host: evil.com' option:

$ curl 'https://forwarded-host-demo-default.herokuapp.com/welcome/foo' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Accept: text/html, application/xhtml+xml' -H 'Referer: https://forwarded-host-demo-default.herokuapp.com/' -H 'Cookie: _forwarded_host_session=UDBEQXhHclAvY3phbmx1VWRiZWhKV3FMUU9HcmNwU291SHZyOW9xNlJZblAzSW1acE8xYW4zMlcyU2taMUEvcnRObW1BdkJJeEJsalovOWRrejNkOWE2ZWRVeDUzdVhsVUVzKzRnY1Jad1A3L0xnbHRvZ0dxRDZxMDZtZFlXcGx2VFJxQTZ0ekJNb3E4ZVBRSWovaHVnPT0tLWE3WVlTZEtvRjVXUnNNVDQ1MVhnd0E9PQ%3D%3D--a6835674c0bf2075c786df489bafd0b0191b4880' -H 'Connection: keep-alive' -H 'Turbolinks-Referrer: https://forwarded-host-demo-default.herokuapp.com/' --compressed -H 'X-Forwarded-Host: evil.com'

the response is still

<html><body>You are being <a href="https://evil.com/">redirected</a>.</body></html>

In ankane/secure_rails#4, you also suggested that we try to render root_url, which we can do by curling the root with the evil X-Forwarded-Host header. That does indeed render the default value (set to forwarded-host-demo.herokuapp.com on the branch) for root_url, even though request.host is evil.com:

$ curl 'https://forwarded-host-demo-default.herokuapp.com/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Accept: text/html, application/xhtml+xml' -H 'Referer: https://forwarded-host-demo-default.herokuapp.com/' -H 'Cookie: _forwarded_host_session=UDBEQXhHclAvY3phbmx1VWRiZWhKV3FMUU9HcmNwU291SHZyOW9xNlJZblAzSW1acE8xYW4zMlcyU2taMUEvcnRObW1BdkJJeEJsalovOWRrejNkOWE2ZWRVeDUzdVhsVUVzKzRnY1Jad1A3L0xnbHRvZ0dxRDZxMDZtZFlXcGx2VFJxQTZ0ekJNb3E4ZVBRSWovaHVnPT0tLWE3WVlTZEtvRjVXUnNNVDQ1MVhnd0E9PQ%3D%3D--a6835674c0bf2075c786df489bafd0b0191b4880' -H 'Connection: keep-alive' -H 'Turbolinks-Referrer: https://forwarded-host-demo-default.herokuapp.com/' --compressed -H 'X-Forwarded-Host: evil.com'
<!DOCTYPE html>
...
<pre>
  HTTP_X_FORWARDED_HOST = evil.com
  request.host = evil.com
  root_url = https://forwarded-host-demo.herokuapp.com/
</pre>
<a href="/welcome/foo">redirect back to home page</a>
...

If I make the same request to the app without the default_url_options workaround, the root_url is again on evil.com:

curl 'http://forwarded-host-demo.herokuapp.com/' -H 'If-None-Match: W/"254e9ac6e6fdf70d3db343af2e14544f"' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' -H 'Cache-Control: max-age=0' -H 'Cookie: _forwarded_host_session=MHNLQ3IzRUU2bXpyNEhFdSt5M3M4MzV3VkpPTnF2emJ6NFJWeFlJYXB2dmdlQ1IzeHNsMXpqUXJNdHFwWndFZ05hcmxvZloxRi8vejdnYzNEclNrK1VGdnVzZGpVM0RnMFFkUVdsL1V5cHh0akpSQkNXN0xWUURWYU5GRVFrWVhZVVZGZWdoVkxaSE5iclIvY29DK29BPT0tLUhhSlpFQk53clFDL0FrQnNpZW9zRnc9PQ%3D%3D--90e765814b25d1b640f35dce1d4ed9b15716ac09' -H 'Connection: keep-alive' --compressed  -H 'X-Forwarded-Host: evil.com'
<!DOCTYPE html>
...
<pre>
  HTTP_X_FORWARDED_HOST = evil.com
  request.host = evil.com
  root_url = http://evil.com/
</pre>
<a href="/welcome/foo">redirect back to home page</a>
...

So, to sum up, I think this shows that:

  1. In the default configuration, both root_url and request.host can be controlled by the attacker. (Edit: As pointed out above, the bad root_urls used in views can be cached in the rails page cache, which seems like the most serious problem.)

  2. The default_url_options workaround brings root_url back under the application's control, but request.host remains under the attacker's control, which leaves possible open redirects. (Edit: Rails page caching won't cache these redirects, but downstream caches might, which was the issue mentioned in https://hackerone.com/reports/487 . I'd therefore say this workaround is a good one, but it is not a complete workaround.)

Finally, I'd also note that the other workaround I suggested, which was to strip the X-Forwarded-Host header, is also implemented in this gem: https://github.com/pusher/rack-headers_filter --- they also seem to have had this problem and found the same solution (strip the headers).

I'll also edit my comment above to make note of these points. Thanks again!

@rails-bot

This comment has been minimized.

Show comment
Hide comment
@rails-bot

rails-bot bot Nov 1, 2017

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 5-1-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

rails-bot bot commented Nov 1, 2017

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 5-1-stable branch or on master, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

@rafaelfranca rafaelfranca added pinned security and removed stale labels Nov 1, 2017

monfresh added a commit to codeforamerica/ohana-api that referenced this issue Feb 4, 2018

Specify allowed hosts for assets and URLs
**Why**: To help prevent host header injection.
See: https://github.com/ankane/secure_rails and
rails/rails#29893

monfresh added a commit to codeforamerica/ohana-api that referenced this issue Feb 4, 2018

Specify allowed hosts for assets and URLs
**Why**: To help prevent host header injection.
See: https://github.com/ankane/secure_rails and
rails/rails#29893

monfresh added a commit to codeforamerica/ohana-api that referenced this issue Feb 4, 2018

Specify allowed hosts for assets and URLs
**Why**: To help prevent host header injection.
See: https://github.com/ankane/secure_rails and
rails/rails#29893

monfresh added a commit to codeforamerica/ohana-api that referenced this issue Feb 4, 2018

Specify allowed hosts for assets and URLs
**Why**: To help prevent host header injection.
See: https://github.com/ankane/secure_rails and
rails/rails#29893

monfresh added a commit to codeforamerica/ohana-api that referenced this issue Feb 5, 2018

Specify allowed hosts for assets and URLs
**Why**: To help prevent host header injection.
See: https://github.com/ankane/secure_rails and
rails/rails#29893
@m1foley

This comment has been minimized.

Show comment
Hide comment
@m1foley

m1foley May 10, 2018

A vulnerability scanner just hit my site with this exploit. The redirect was implemented like get "/foo", to: redirect("/bar") and this is still an issue on Rails 5.1.6.

m1foley commented May 10, 2018

A vulnerability scanner just hit my site with this exploit. The redirect was implemented like get "/foo", to: redirect("/bar") and this is still an issue on Rails 5.1.6.

@david-a-wheeler

This comment has been minimized.

Show comment
Hide comment
@david-a-wheeler

david-a-wheeler May 29, 2018

Shouldn't this have a CVE number assigned to the Rails framework? If the Rails framework by default trusts the attacker for key functions, and that is not clearly documented on every function (e.g., "do not use"), then it seems like this deserves to be tracked as a vulnerability.

david-a-wheeler commented May 29, 2018

Shouldn't this have a CVE number assigned to the Rails framework? If the Rails framework by default trusts the attacker for key functions, and that is not clearly documented on every function (e.g., "do not use"), then it seems like this deserves to be tracked as a vulnerability.

@soundasleep

This comment has been minimized.

Show comment
Hide comment
@soundasleep

soundasleep Sep 3, 2018

I'm surprised that this is still an issue, that it still doesn't have a CVE, and that this assumption (that Rails can use client-supplied X-Forwarded-For to generate redirection URLs) is still unsafely present.

We've found that defining a whitelist of valid hosts works best, by blocking malicious clients setting either Host or X-Forwarded-For headers. This works better than rack-headers_filter when using reverse proxies, and also handles a malicious client setting Host.

Include this in your ApplicationController:

  before_action :block_unknown_hosts

  SAFE_HOSTS = [
    "",
    "domain.com",
    "www.domain.com",
    "localhost", # for reverse proxy
  ]

  class NotASafeHostError < StandardError; end

  # We need to be able to trust the provided Host header, so that we can safely redirect
  def block_unknown_hosts
    unless SAFE_HOSTS.include?(request.host)
      raise NotASafeHostError, "#{request.host} is not a safe host"
    end
  end

soundasleep commented Sep 3, 2018

I'm surprised that this is still an issue, that it still doesn't have a CVE, and that this assumption (that Rails can use client-supplied X-Forwarded-For to generate redirection URLs) is still unsafely present.

We've found that defining a whitelist of valid hosts works best, by blocking malicious clients setting either Host or X-Forwarded-For headers. This works better than rack-headers_filter when using reverse proxies, and also handles a malicious client setting Host.

Include this in your ApplicationController:

  before_action :block_unknown_hosts

  SAFE_HOSTS = [
    "",
    "domain.com",
    "www.domain.com",
    "localhost", # for reverse proxy
  ]

  class NotASafeHostError < StandardError; end

  # We need to be able to trust the provided Host header, so that we can safely redirect
  def block_unknown_hosts
    unless SAFE_HOSTS.include?(request.host)
      raise NotASafeHostError, "#{request.host} is not a safe host"
    end
  end
@albinowax

This comment has been minimized.

Show comment
Hide comment
@albinowax

albinowax Sep 13, 2018

You might find the following paper interesting - in it I explain in depth how the X-Forwarded-Host header can be used to exploit numerous real systems: https://portswigger.net/blog/practical-web-cache-poisoning

Zend, Symfony and Drupal had very a similar issue, and resolved it with a coordinated security release. As such I'm a little surprised to find this has been languishing in a public issue tracker.

albinowax commented Sep 13, 2018

You might find the following paper interesting - in it I explain in depth how the X-Forwarded-Host header can be used to exploit numerous real systems: https://portswigger.net/blog/practical-web-cache-poisoning

Zend, Symfony and Drupal had very a similar issue, and resolved it with a coordinated security release. As such I'm a little surprised to find this has been languishing in a public issue tracker.

@gsamokovarov

This comment has been minimized.

Show comment
Hide comment
@gsamokovarov

gsamokovarov Sep 13, 2018

Contributor

We already have a bit of work-in-progress code to guard against basic host header attacks in #33145. If you folks have ideas how we can handle the X-Forwarded-For or X-Forwarded-Host headers, please consider dropping a note in the PR above.

Contributor

gsamokovarov commented Sep 13, 2018

We already have a bit of work-in-progress code to guard against basic host header attacks in #33145. If you folks have ideas how we can handle the X-Forwarded-For or X-Forwarded-Host headers, please consider dropping a note in the PR above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment