Skip to content

Commit

Permalink
Capture user's actual IP when rate limiting in QA and Live where we u…
Browse files Browse the repository at this point in the history
…se CloudFlare (#1920)

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 8, 2024
1 parent 32ab692 commit b3db65b
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 10 deletions.
29 changes: 19 additions & 10 deletions config/initializers/rack_attack.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
class Rack::Attack
redis = Redis.new(REDIS_CONFIG)

# Extract real IP address from Cloudflare header if present
def self.real_ip(req)
req.get_header('HTTP_CF_CONNECTING_IP') || req.ip
end

# 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'
real_ip(req) if req.path == '/api/graphql'
end

# Blocklist IP addresses that are permanently blocked
blocklist('block aggressive IPs') do |req|
redis.get("block:#{req.ip}") == "true"
redis.get("block:#{real_ip(req)}") == "true"
end

# Track excessive login attempts for permanent blocking
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
ip = real_ip(req)
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 b3db65b

Please sign in to comment.