From 336adb9bfdc28f7d6bbc5a78f89b11d1d2de4359 Mon Sep 17 00:00:00 2001 From: Riccardo Graziosi <31478034+riggraz@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:43:37 +0100 Subject: [PATCH] Add DDoS protection (#308) * Add and configure rack-attack gem * Limit number of tenant registrations with same email address * Limit requests to tenants#create by IP --- Gemfile | 3 ++ Gemfile.lock | 3 ++ app/controllers/tenants_controller.rb | 4 ++ config/initializers/rack_attack.rb | 73 +++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 config/initializers/rack_attack.rb diff --git a/Gemfile b/Gemfile index ccf51d98..14a1406f 100644 --- a/Gemfile +++ b/Gemfile @@ -41,6 +41,9 @@ gem 'react-rails', '2.6.2' # Pagination gem 'kaminari', '1.2.2' +# DDoS protection +gem 'rack-attack', '6.7.0' + group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 2977ed1e..41529c5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -170,6 +170,8 @@ GEM activesupport (>= 3.0.0) racc (1.7.3) rack (2.2.8.1) + rack-attack (6.7.0) + rack (>= 1.0, < 4) rack-test (2.1.0) rack (>= 1.3) rails (6.1.7.7) @@ -293,6 +295,7 @@ DEPENDENCIES pg (= 1.3.5) puma (= 5.6.8) pundit (= 2.2.0) + rack-attack (= 6.7.0) rails (= 6.1.7.7) rake (= 12.3.3) react-rails (= 2.6.2) diff --git a/app/controllers/tenants_controller.rb b/app/controllers/tenants_controller.rb index 3d499eb0..ec47586c 100644 --- a/app/controllers/tenants_controller.rb +++ b/app/controllers/tenants_controller.rb @@ -29,6 +29,10 @@ class TenantsController < ApplicationController @tenant.status = "active" # no need to verify email address if logged in with oauth end + # Check how many times this email registered a tenant + already_registered_tenants = User.unscoped.where(email: params[:user][:email], role: User.roles[:owner]).count + raise "Too many tenants registered by email" unless already_registered_tenants < 3 + @tenant.save! Current.tenant = @tenant diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 00000000..a41d5fbf --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,73 @@ +class Rack::Attack + ### 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: 5.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 /users/sign_in 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 == '/users/sign_in' && req.post? + req.ip + end + end + + # Throttle POST requests to /users/sign_in 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 == '/users/sign_in' && 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 + + # Throttle POST requests to /tenants by IP address + throttle('tenant_signups/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/tenants' && req.post? + req.ip + 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 \ No newline at end of file