Skip to content

Commit

Permalink
Capture user's actual IP when rate limiting traffic behind Cloudflare (
Browse files Browse the repository at this point in the history
…#1911)

When behind cloudflare, Rack::Attack receive's cloudflare's IPs
instead of the user's actual IP address. Here, we are changing
the configuration to consume HTTP_CF_CONNECTING_IP if it is
available.
  • Loading branch information
jayjay-w authored Jun 7, 2024
1 parent 081a638 commit aa8d073
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 7 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ gem 'graphql-formatter'
gem 'nokogiri', '1.16.5'
gem 'puma'
gem 'rack-attack'
gem 'rack-cloudflare'
gem 'rack-cors', '1.0.6', require: 'rack/cors'
gem 'sidekiq', '5.2.10'
gem 'sidekiq-cloudwatchmetrics'
Expand Down
3 changes: 3 additions & 0 deletions config/initializers/cloudflare.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if Rails.env.production?
Rails.application.config.middleware.insert_before Rack::Attack, Rack::Cloudflare
end
18 changes: 11 additions & 7 deletions config/initializers/rack_attack.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Rack::Attack
redis = Redis.new(REDIS_CONFIG)

# Throttle all graphql requests by IP address

throttle('api/graphql', limit: proc { CheckConfig.get('api_rate_limit', 100, :integer) }, period: 60.seconds) do |req|
req.ip if req.path == '/api/graphql'
end
Expand All @@ -15,13 +15,17 @@ class Rack::Attack
track('track excessive logins/ip') do |req|
if req.path == '/api/users/sign_in' && req.post?
ip = req.ip
# Increment the counter for the IP and check if it should be blocked
count = redis.incr("track:#{ip}")
redis.expire("track:#{ip}", 3600) # Set the expiration time to 1 hour
begin
# Increment the counter for the IP and check if it should be blocked
count = redis.incr("track:#{ip}")
redis.expire("track:#{ip}", 3600) # Set the expiration time to 1 hour

# Add IP to blocklist if count exceeds the threshold
if count.to_i >= CheckConfig.get('login_block_limit', 100, :integer)
redis.set("block:#{ip}", true) # No expiration
# Add IP to blocklist if count exceeds the threshold
if count.to_i >= CheckConfig.get('login_block_limit', 100, :integer)
redis.set("block:#{ip}", true) # No expiration
end
rescue => e
Rails.logger.error("Rack::Attack Error: #{e.message}")
end

ip
Expand Down
28 changes: 28 additions & 0 deletions test/lib/check_rack_attack_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,32 @@ class ThrottlingTest < ActionDispatch::IntegrationTest
assert_response :forbidden
end
end

test "should handle requests via Cloudflare correctly in production" do
original_env = Rails.env
Rails.env = 'production'

stub_configs({ 'api_rate_limit' => 3, 'login_block_limit' => 2 }) do
# Test throttling for /api/graphql via Cloudflare
3.times do
post api_graphql_path, headers: { 'CF-Connecting-IP' => '1.2.3.4' }
assert_response :unauthorized
end

post api_graphql_path, headers: { 'CF-Connecting-IP' => '1.2.3.4' }
assert_response :too_many_requests

# Test blocking for /api/users/sign_in via Cloudflare
user_params = { api_user: { email: '[email protected]', password: random_complex_password } }

2.times do
post api_user_session_path, params: user_params, as: :json, headers: { 'CF-Connecting-IP' => '1.2.3.4' }
end

post api_user_session_path, params: user_params, as: :json, headers: { 'CF-Connecting-IP' => '1.2.3.4' }
assert_response :forbidden
end

Rails.env = original_env
end
end

0 comments on commit aa8d073

Please sign in to comment.