chore: Block & throttle abusive requests (#2706)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
359c3c8ccb
commit
b44f9b792b
6 changed files with 87 additions and 10 deletions
|
@ -147,6 +147,10 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||||
# maxmindb api key to use geoip2 service
|
# maxmindb api key to use geoip2 service
|
||||||
# IP_LOOKUP_API_KEY=
|
# IP_LOOKUP_API_KEY=
|
||||||
|
|
||||||
|
## Rack Attack configuration
|
||||||
|
## To prevent and throttle abusive requests
|
||||||
|
# ENABLE_RACK_ATTACK=false
|
||||||
|
|
||||||
|
|
||||||
## Running chatwoot as an API only server
|
## Running chatwoot as an API only server
|
||||||
## setting this value to true will disable the frontend dashboard endpoints
|
## setting this value to true will disable the frontend dashboard endpoints
|
||||||
|
|
|
@ -29,7 +29,8 @@ Style/OptionalBooleanParameter:
|
||||||
- 'app/dispatchers/dispatcher.rb'
|
- 'app/dispatchers/dispatcher.rb'
|
||||||
Style/GlobalVars:
|
Style/GlobalVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/redis.rb'
|
- 'config/initializers/01_redis.rb'
|
||||||
|
- 'config/initializers/rack_attack.rb'
|
||||||
- 'lib/redis/alfred.rb'
|
- 'lib/redis/alfred.rb'
|
||||||
- 'lib/global_config.rb'
|
- 'lib/global_config.rb'
|
||||||
Style/ClassVars:
|
Style/ClassVars:
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -33,6 +33,8 @@ gem 'liquid'
|
||||||
gem 'commonmarker'
|
gem 'commonmarker'
|
||||||
# Validate Data against JSON Schema
|
# Validate Data against JSON Schema
|
||||||
gem 'json_schemer'
|
gem 'json_schemer'
|
||||||
|
# Rack middleware for blocking & throttling abusive requests
|
||||||
|
gem 'rack-attack'
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
|
@ -45,7 +47,6 @@ gem 'groupdate'
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
gem 'redis'
|
gem 'redis'
|
||||||
gem 'redis-namespace'
|
gem 'redis-namespace'
|
||||||
gem 'redis-rack-cache'
|
|
||||||
# super fast record imports in bulk
|
# super fast record imports in bulk
|
||||||
gem 'activerecord-import'
|
gem 'activerecord-import'
|
||||||
|
|
||||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -376,8 +376,8 @@ GEM
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.5.2)
|
racc (1.5.2)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-cache (1.12.0)
|
rack-attack (6.5.0)
|
||||||
rack (>= 0.4)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-proxy (0.6.5)
|
rack-proxy (0.6.5)
|
||||||
|
@ -419,11 +419,6 @@ GEM
|
||||||
redis (4.2.1)
|
redis (4.2.1)
|
||||||
redis-namespace (1.8.0)
|
redis-namespace (1.8.0)
|
||||||
redis (>= 3.0.4)
|
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)
|
regexp_parser (1.7.1)
|
||||||
representable (3.0.4)
|
representable (3.0.4)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
|
@ -666,12 +661,12 @@ DEPENDENCIES
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails
|
rails
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
redis-rack-cache
|
|
||||||
responders
|
responders
|
||||||
rest-client
|
rest-client
|
||||||
rspec-rails (~> 4.0.0.beta2)
|
rspec-rails (~> 4.0.0.beta2)
|
||||||
|
|
|
@ -4,3 +4,7 @@ redis = Rails.env.test? ? MockRedis.new : Redis.new(Redis::Config.app)
|
||||||
# Add here as you use it for more features
|
# Add here as you use it for more features
|
||||||
# Used for Round Robin, Conversation Emails & Online Presence
|
# Used for Round Robin, Conversation Emails & Online Presence
|
||||||
$alfred = Redis::Namespace.new('alfred', redis: redis, warning: true)
|
$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)
|
72
config/initializers/rack_attack.rb
Normal file
72
config/initializers/rack_attack.rb
Normal file
|
@ -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))
|
Loading…
Reference in a new issue