Skip to content
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

Trust X-Forwarded-For from the right to the left #162

Merged
merged 1 commit into from
Oct 7, 2024

Conversation

geoffharcourt
Copy link
Contributor

In #149 we got the extremely helpful Request#cloudflare? method which can assist with checking if a request has been proxied through Cloudflare. The method checks that there's an unbroken chain of trust from the REMOTE_ADDR (if it exists) through forwarded IPs that only includes trusted internal proxies and Cloudflare IP addresses.

Each proxy in the chain is supposed to append the IP of the traffic it received to the right side of X-Forwarded-For. This means that after evaluating REMOTE_ADDR we should be looking at addresses in X-Forwarded-For starting on the right side, not on the left.

trusted_proxies = (remote_addresses + forwarded_for).take_while do |ip|
trusted_proxy?(ip)
end

This ordering means that if a request has the original client as the first address in X-Forwarded-For that we'll incorrectly reject it. It also means that an attacker could craft a forged X-Forwarded-For that starts with a Cloudflare IP and smuggle in a request that appears to be valid to the method's logic.

The fix here is to continue to look at REMOTE_ADDR first but then to look at X-Forwarded-For in right-to-left order until we run out of trusted IP addresses (whether internal proxies or Cloudflare IPs).

Fix #161

In modosc#149 we got the extremely helpful `Request#cloudflare?` method which
can assist with checking if a request has been proxied through
Cloudflare. The method checks that there's an unbroken chain of trust
from the `REMOTE_ADDR` (if it exists) through forwarded IPs that only
includes trusted internal proxies and Cloudflare IP addresses.

Each proxy in the chain is supposed to append the IP of the traffic it
received to the *right side* of `X-Forwarded-For`. This means that after
evaluating `REMOTE_ADDR` we should be looking at addresses in
`X-Forwarded-For` starting on the right side, not on the left.

https://github.com/modosc/cloudflare-rails/blob/311875eef57862ecf050fb87415c56ea4823486a/lib/cloudflare_rails/check_trusted_proxies.rb#L23-L25

This ordering means that if a request has the original client as the
first address in `X-Forwarded-For` that we'll incorrectly reject it. It
also means that an attacker could craft a forged `X-Forwarded-For` that
starts with a Cloudflare IP and smuggle in a request that appears to be
valid to the method's logic.

The fix here is to continue to look at `REMOTE_ADDR` first but then to
look at `X-Forwarded-For` in right-to-left order until we run out of
trusted IP addresses (whether internal proxies or Cloudflare IPs).

Fix modosc#161
```ruby
config.cloudflare.expires_in = 12.hours # default value
config.cloudflare.timeout = 5.seconds # default value
```

## Blocking non-Cloudflare traffic
You can use the `#cloudfront?` method from this gem to block all non-Cloudflare traffic to your application. Here's an example of doing this with [`Rack::Attack`](https://github.com/rack/rack-attack):

You can use the `#cloudflare?` method from this gem to block all non-Cloudflare traffic to your application. Here's an example of doing this with [`Rack::Attack`](https://github.com/rack/rack-attack):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo here.

- `REMOTE_ADDR: CloudFlare`
- `REMOTE_ADDR: trusted_proxy`, `X_HTTP_FORWARDED_FOR: CloudFlare`
- `REMOTE_ADDR: trusted_proxy`, `X_HTTP_FORWARDED_FOR: CloudFlare,trusted_proxy2`
- `REMOTE_ADDR: trusted_proxy`, `X_HTTP_FORWARDED_FOR: untrusted,CloudFlare`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to show the expectations.

@@ -139,11 +144,6 @@
it 'returns false if the request has a trusted REMOTE_ADDR but did not originate from CloudFlare' do
expect(Rack::Request.new('REMOTE_ADDR' => '10.1.1.1', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4')).not_to be_cloudflare
end

it 'returns false if the request has a trusted REMOTE_ADDR and an untrusted proxy before CloudFlare' do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec should be true, moved it up and reworded.

@@ -122,9 +122,14 @@
'HTTP_X_FORWARDED_FOR' => '10.2.2.2,197.234.240.1')).to be_cloudflare
end

it 'returns true if the request originated from CloudFlare via one trusted proxy and one untrusted upstream IP' do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec should be false, reworded and moved it down.

@modosc modosc merged commit 0430424 into modosc:main Oct 7, 2024
4 checks passed
@geoffharcourt geoffharcourt deleted the x-forwarded-for-order branch October 7, 2024 18:06
@geoffharcourt
Copy link
Contributor Author

Thanks for the quick review and merge @modosc!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Possible issue with client IP in X-Forwarded-For when checking #cloudflare?
2 participants