Skip to content

Commit

Permalink
Add cloudflare? method to determine if request passed through CF (#149)
Browse files Browse the repository at this point in the history
* Add cloudflare? method to determine if request passed through CF

The `request.cloudflare?` method can be used in a Rack::Attack blocklist rule
to block traffic that hasn't passed through CloudFlare.

For instance:

```ruby
  Rack::Attack.blocklist('CloudFlare WAF bypass') do |req|
    !req.cloudflare?
  end
```

Note that the request may optionally pass through additional trusted
proxies, so it will return true for any of these scenarios:

* `REMOTE_ADDR` = CloudFlare
* `REMOTE_ADDR` = *trusted_proxy*, `X_HTTP_FORWARDED_FOR` = CloudFlare,...
* `REMOTE_ADDR` = *trusted_proxy*, `X_HTTP_FORWARDED_FOR` = *trusted_proxy2*,CloudFlare,...

but it will return false if CloudFlare comes after the trusted prefix of
`X-Forwarded-For`.

* Address Rubocop warnings

* Break up tests to only have one expectation per spec
* Reduce line lengths
  • Loading branch information
afn authored Sep 17, 2024
1 parent b939127 commit f43de94
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 3 deletions.
23 changes: 20 additions & 3 deletions lib/cloudflare_rails/check_trusted_proxies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,29 @@ module CloudflareRails
# patch rack::request::helpers to use our cloudflare ips - this way request.ip is
# correct inside of rack and rails
module CheckTrustedProxies
def trusted_proxy?(ip)
matching = Importer.cloudflare_ips.any? do |proxy|
def cloudflare_ip?(ip)
Importer.cloudflare_ips.any? do |proxy|
proxy === ip
rescue IPAddr::InvalidAddressError
end
matching || super
end

def trusted_proxy?(ip)
cloudflare_ip?(ip) || super
end

def cloudflare?
remote_addresses = split_header(get_header('REMOTE_ADDR'))
forwarded_for = self.forwarded_for || []

# Select only the trusted prefix of REMOTE_ADDR + X_HTTP_FORWARDED_FOR
trusted_proxies = (remote_addresses + forwarded_for).take_while do |ip|
trusted_proxy?(ip)
end

trusted_proxies.any? do |ip|
cloudflare_ip?(ip)
end
end
end
end
44 changes: 44 additions & 0 deletions spec/cloudflare/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,50 @@
end
end

describe 'Rack::Request' do
before do
rails_app.initialize!
end

describe '#cloudflare?' do
it 'returns true if the request originated from CloudFlare directly' do
expect(Rack::Request.new('REMOTE_ADDR' => '197.234.240.1')).to be_cloudflare
end

it 'returns true if the request originated from CloudFlare via one trusted proxy' do
expect(Rack::Request.new('REMOTE_ADDR' => '10.1.1.1', 'HTTP_X_FORWARDED_FOR' => '197.234.240.1')).to be_cloudflare
end

it 'returns true if the request originated from CloudFlare via two trusted proxies' do
expect(Rack::Request.new('REMOTE_ADDR' => '10.1.1.1',
'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
expect(Rack::Request.new('REMOTE_ADDR' => '10.1.1.1',
'HTTP_X_FORWARDED_FOR' => '197.234.240.1,1.2.3.4')).to be_cloudflare
end

it 'returns false if the request did not originate from CloudFlare' do
expect(Rack::Request.new('REMOTE_ADDR' => '1.2.3.4')).not_to be_cloudflare
end

it 'returns false if the request originated from CloudFlare via an untrusted REMOTE_ADDR' do
expect(Rack::Request.new('REMOTE_ADDR' => '1.2.3.4',
'HTTP_X_FORWARDED_FOR' => '197.234.240.1')).not_to be_cloudflare
end

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
expect(Rack::Request.new('REMOTE_ADDR' => '10.1.1.1',
'HTTP_X_FORWARDED_FOR' => '1.2.3.4,197.234.240.1')).not_to be_cloudflare
end
end
end

# functional tests - maybe duplicate of the remote_ip/ip tests above?
describe 'middleware', type: :request do
let(:base_ip) { '1.2.3.4' }
Expand Down

0 comments on commit f43de94

Please sign in to comment.