From b44f9b792b1af87a4af09d7a16341f5bcb643eaf Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 27 Jul 2021 21:27:23 +0530 Subject: [PATCH] chore: Block & throttle abusive requests (#2706) Co-authored-by: Pranav Raj S --- .env.example | 4 ++ .rubocop.yml | 3 +- Gemfile | 3 +- Gemfile.lock | 11 +-- config/initializers/{redis.rb => 01_redis.rb} | 4 ++ config/initializers/rack_attack.rb | 72 +++++++++++++++++++ 6 files changed, 87 insertions(+), 10 deletions(-) rename config/initializers/{redis.rb => 01_redis.rb} (67%) create mode 100644 config/initializers/rack_attack.rb diff --git a/.env.example b/.env.example index 6dffe6b38..ea246c6f8 100644 --- a/.env.example +++ b/.env.example @@ -147,6 +147,10 @@ USE_INBOX_AVATAR_FOR_BOT=true # maxmindb api key to use geoip2 service # IP_LOOKUP_API_KEY= +## Rack Attack configuration +## To prevent and throttle abusive requests +# ENABLE_RACK_ATTACK=false + ## Running chatwoot as an API only server ## setting this value to true will disable the frontend dashboard endpoints diff --git a/.rubocop.yml b/.rubocop.yml index 4bec94c82..133f8e15c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -29,7 +29,8 @@ Style/OptionalBooleanParameter: - 'app/dispatchers/dispatcher.rb' Style/GlobalVars: Exclude: - - 'config/initializers/redis.rb' + - 'config/initializers/01_redis.rb' + - 'config/initializers/rack_attack.rb' - 'lib/redis/alfred.rb' - 'lib/global_config.rb' Style/ClassVars: diff --git a/Gemfile b/Gemfile index 0f285c6fd..29fbb8af2 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,8 @@ gem 'liquid' gem 'commonmarker' # Validate Data against JSON Schema gem 'json_schemer' +# Rack middleware for blocking & throttling abusive requests +gem 'rack-attack' ##-- for active storage --## gem 'aws-sdk-s3', require: false @@ -45,7 +47,6 @@ gem 'groupdate' gem 'pg' gem 'redis' gem 'redis-namespace' -gem 'redis-rack-cache' # super fast record imports in bulk gem 'activerecord-import' diff --git a/Gemfile.lock b/Gemfile.lock index 86d421fa7..fc581ae27 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -376,8 +376,8 @@ GEM raabro (1.4.0) racc (1.5.2) rack (2.2.3) - rack-cache (1.12.0) - rack (>= 0.4) + rack-attack (6.5.0) + rack (>= 1.0, < 3) rack-cors (1.1.1) rack (>= 2.0.0) rack-proxy (0.6.5) @@ -419,11 +419,6 @@ GEM redis (4.2.1) redis-namespace (1.8.0) redis (>= 3.0.4) - redis-rack-cache (2.2.1) - rack-cache (>= 1.10, < 2) - redis-store (>= 1.6, < 2) - redis-store (1.9.0) - redis (>= 4, < 5) regexp_parser (1.7.1) representable (3.0.4) declarative (< 0.1.0) @@ -666,12 +661,12 @@ DEPENDENCIES pry-rails puma pundit + rack-attack rack-cors rack-timeout rails redis redis-namespace - redis-rack-cache responders rest-client rspec-rails (~> 4.0.0.beta2) diff --git a/config/initializers/redis.rb b/config/initializers/01_redis.rb similarity index 67% rename from config/initializers/redis.rb rename to config/initializers/01_redis.rb index 1566098c9..c5607bb2c 100644 --- a/config/initializers/redis.rb +++ b/config/initializers/01_redis.rb @@ -4,3 +4,7 @@ redis = Rails.env.test? ? MockRedis.new : Redis.new(Redis::Config.app) # Add here as you use it for more features # Used for Round Robin, Conversation Emails & Online Presence $alfred = Redis::Namespace.new('alfred', redis: redis, warning: true) + +# Velma : Determined protector +# used in rack attack +$velma = Redis::Namespace.new('velma', redis: redis, warning: true) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..3070d2877 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,72 @@ +class Rack::Attack + ### Configure Cache ### + + # If you don't want to use Rails.cache (Rack::Attack's default), then + # configure it here. + # + # Note: The store is only used for throttling (not blocklisting and + # safelisting). It must implement .increment and .write like + # ActiveSupport::Cache::Store + + # Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + + # https://github.com/rack/rack-attack/issues/102 + Rack::Attack.cache.store = Rack::Attack::StoreProxy::RedisProxy.new($velma) + + class Request < ::Rack::Request + # You many need to specify a method to fetch the correct remote IP address + # if the web server is behind a load balancer. + def remote_ip + @remote_ip ||= (env['action_dispatch.remote_ip'] || ip).to_s + end + + def allowed_ip? + allowed_ips = ['127.0.0.1', '::1'] + allowed_ips.include?(remote_ip) + end + end + + ### Throttle Spammy Clients ### + + # If any single client IP is making tons of requests, then they're + # probably malicious or a poorly-configured scraper. Either way, they + # don't deserve to hog all of the app server's CPU. Cut them off! + # + # Note: If you're serving assets through rack, those requests may be + # counted by rack-attack and this throttle may be activated too + # quickly. If so, enable the condition to exclude them from tracking. + + # Throttle all requests by IP (60rpm) + # + # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" + + throttle('req/ip', limit: 300, period: 1.minute, &:ip) + + ### Prevent Brute-Force Login Attacks ### + throttle('login/ip', limit: 5, period: 20.seconds) do |req| + req.ip if req.path == '/auth/sign_in' && req.post? + end + + ## Prevent Brute-Force Signup Attacks ### + throttle('accounts/ip', limit: 5, period: 5.minutes) do |req| + req.ip if req.path == '/api/v1/accounts' && req.post? + end + + # ref: https://github.com/rack/rack-attack/issues/399 + throttle('login/email', limit: 20, period: 5.minutes) do |req| + email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence + email.to_s.downcase.gsub(/\s+/, '') if req.path == '/auth/sign_in' && req.post? + end + + throttle('reset_password/email', limit: 5, period: 1.hour) do |req| + email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence + email.to_s.downcase.gsub(/\s+/, '') if req.path == '/auth/password' && req.post? + end +end + +# Log blocked events +ActiveSupport::Notifications.subscribe('throttle.rack_attack') do |_name, _start, _finish, _request_id, payload| + Rails.logger.info "[Rack::Attack][Blocked] remote_ip: \"#{payload[:request].remote_ip}\", path: \"#{payload[:request].path}\"" +end + +Rack::Attack.enabled = ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_RACK_ATTACK', false))