]> git.openstreetmap.org Git - rails.git/commitdiff
Add support for rate limiting signup requests
authorTom Hughes <tom@compton.nu>
Tue, 22 Aug 2023 17:32:53 +0000 (18:32 +0100)
committerTom Hughes <tom@compton.nu>
Tue, 22 Aug 2023 17:45:17 +0000 (18:45 +0100)
app/controllers/users_controller.rb
config/initializers/rate_limits.rb [new file with mode: 0644]
config/settings.yml
lib/rate_limiter.rb [new file with mode: 0644]

index 0f9e1676764d11d5efb165254b798bc228bb0b2a..9d4b3d258cce6c098d288a39c7ae13d2743eaab0 100644 (file)
@@ -186,6 +186,9 @@ class UsersController < ApplicationController
         end
 
         if current_user.save
+          SIGNUP_IP_LIMITER&.update(request.remote_ip)
+          SIGNUP_EMAIL_LIMITER&.update(canonical_email(current_user.email))
+
           flash[:matomo_goal] = Settings.matomo["goals"]["signup"] if defined?(Settings.matomo)
 
           referer = welcome_path
@@ -344,7 +347,13 @@ class UsersController < ApplicationController
                    domain_mx_servers(domain)
                  end
 
-    if blocked = Acl.no_account_creation(request.remote_ip, :domain => domain, :mx => mx_servers)
+    blocked = Acl.no_account_creation(request.remote_ip, :domain => domain, :mx => mx_servers)
+
+    blocked ||= SIGNUP_IP_LIMITER && !SIGNUP_IP_LIMITER.allow?(request.remote_ip)
+
+    blocked ||= email && SIGNUP_EMAIL_LIMITER && !SIGNUP_EMAIL_LIMITER.allow?(canonical_email(email))
+
+    if blocked
       logger.info "Blocked signup from #{request.remote_ip} for #{email}"
 
       render :action => "blocked"
@@ -353,6 +362,20 @@ class UsersController < ApplicationController
     !blocked
   end
 
+  def canonical_email(email)
+    local_part, domain = if email.nil?
+                           nil
+                         else
+                           email.split("@")
+                         end
+
+    local_part.sub!(/\+.*$/, "")
+
+    local_part.delete!(".") if %w[gmail.com googlemail.com].include?(domain)
+
+    "#{local_part}@#{domain}"
+  end
+
   ##
   # get list of MX servers for a domains
   def domain_mx_servers(domain)
diff --git a/config/initializers/rate_limits.rb b/config/initializers/rate_limits.rb
new file mode 100644 (file)
index 0000000..5caa300
--- /dev/null
@@ -0,0 +1,15 @@
+require "rate_limiter"
+
+SIGNUP_IP_LIMITER = if Settings.memcache_servers && Settings.signup_ip_per_day && Settings.signup_ip_max_burst
+                      RateLimiter.new(
+                        Dalli::Client.new(Settings.memcache_servers, :namespace => "rails:signup:ip"),
+                        86400, Settings.signup_ip_per_day, Settings.signup_ip_max_burst
+                      )
+                    end
+
+SIGNUP_EMAIL_LIMITER = if Settings.memcache_servers && Settings.signup_email_per_day && Settings.signup_email_max_burst
+                         RateLimiter.new(
+                           Dalli::Client.new(Settings.memcache_servers, :namespace => "rails:signup:email"),
+                           86400, Settings.signup_email_per_day, Settings.signup_email_max_burst
+                         )
+                       end
index d9910ce28afbdd00a7c070bdaada6f1911ca8531..8ac27df4085d881493962a11a5d505ccd4eec94c 100644 (file)
@@ -140,3 +140,8 @@ smtp_user_name: null
 smtp_password: null
 # Matomo settings for analytics
 #matomo:
+# Signup rate limits
+#signup_ip_per_day:
+#signup_ip_max_burst:
+#signup_email_per_day:
+#signup_email_max_burst:
diff --git a/lib/rate_limiter.rb b/lib/rate_limiter.rb
new file mode 100644 (file)
index 0000000..438f5a1
--- /dev/null
@@ -0,0 +1,38 @@
+class RateLimiter
+  def initialize(cache, interval, limit, max_burst)
+    @cache = cache
+    @requests_per_second = limit.to_f / interval
+    @burst_limit = max_burst
+  end
+
+  def allow?(key)
+    last_update, requests = @cache.get(key)
+
+    if last_update
+      elapsed = Time.now.to_i - last_update
+
+      requests -= elapsed * @requests_per_second
+    else
+      requests = 0.0
+    end
+
+    requests < @burst_limit
+  end
+
+  def update(key)
+    now = Time.now.to_i
+
+    last_update, requests = @cache.get(key)
+
+    if last_update
+      elapsed = now - last_update
+
+      requests -= elapsed * @requests_per_second
+      requests += 1.0
+    else
+      requests = 1.0
+    end
+
+    @cache.set(key, [now, [requests, 1.0].max])
+  end
+end