diff --git a/Gemfile b/Gemfile index 8cd9cfbe4..87ad98f69 100644 --- a/Gemfile +++ b/Gemfile @@ -65,6 +65,7 @@ gem 'rack-timeout' gem 'pg_search' gem 'crawler_detect' +gem 'rack-attack' # downgrade gem to solve parsing error https://stackoverflow.com/questions/74725359/ruby-on-rails-legacy-application-update-generates-gem-psych-alias-error-psychb gem 'psych', '< 4.0' diff --git a/Gemfile.lock b/Gemfile.lock index 26f7e5ae5..35190194f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,6 +332,8 @@ GEM qonfig (0.28.0) racc (1.7.1) rack (2.2.6.4) + rack-attack (6.7.0) + rack (>= 1.0, < 4) rack-piwik (0.3.0) rack-test (2.1.0) rack (>= 1.3) @@ -512,6 +514,7 @@ DEPENDENCIES poltergeist (= 1.18.1) pry psych (< 4.0) + rack-attack rack-piwik (~> 0.3.0) rack-timeout rails (= 6.1.7.6) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..670424008 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,78 @@ +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 + + ### 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: 3.minutes) do |req| + req.ip # unless req.path.start_with?('/assets') + end + + ### Prevent Brute-Force Login Attacks ### + + # The most common brute-force login attack is a brute-force password + # attack where an attacker simply tries a large number of emails and + # passwords to see if any credentials match. + # + # Another common method of attack is to use a swarm of computers with + # different IPs to try brute-forcing a password for a specific account. + + # Throttle POST requests to /login by IP address + # + # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" + throttle('logins/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/login' && req.post? + req.ip + end + end + + # Throttle POST requests to /login by email param + # + # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{normalized_email}" + # + # Note: This creates a problem where a malicious user could intentionally + # throttle logins for another user and force their login requests to be + # denied, but that's not very common and shouldn't happen to you. (Knock + # on wood!) + throttle('logins/email', limit: 5, period: 20.seconds) do |req| + if req.path == '/login' && req.post? + # Normalize the email, using the same logic as your authentication process, to + # protect against rate limit bypasses. Return the normalized email if present, nil otherwise. + req.params['email'].to_s.downcase.gsub(/\s+/, "").presence + end + end + + ### Custom Throttle Response ### + + # By default, Rack::Attack returns an HTTP 429 for throttled responses, + # which is just fine. + # + # If you want to return 503 so that the attacker might be fooled into + # believing that they've successfully broken your app (or you just want to + # customize the response), then uncomment these lines. + # self.throttled_response = lambda do |env| + # [ 503, # status + # {}, # headers + # ['']] # body + # end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ae772b321..5ba412f2c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,7 @@ require File.expand_path('../config/environment', __dir__) require 'rspec/rails' require 'capybara/poltergeist' +Rack::Attack.enabled = false Capybara.javascript_driver = :poltergeist Capybara.default_max_wait_time = 10 # Requires supporting ruby files with custom matchers and macros, etc,