diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index dd5d86639f..ce88316761 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -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 diff --git a/test/lib/check_rack_attack_test.rb b/test/lib/check_rack_attack_test.rb index eb294d866a..a467dcab4a 100644 --- a/test/lib/check_rack_attack_test.rb +++ b/test/lib/check_rack_attack_test.rb @@ -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: 'user@example.com', 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