CVE-2022-3741: Improper Restriction of Excessive Authentication Attempts (CWE-307)
value: Impact varies for each individual vulnerability in the application. For generation of accounts, it may be possible, depending on the amount of system resources available, to create a DoS event in the server. These accounts still need to be activated; however, it is possible to identify the output Status Code to separate accounts that are generated and waiting for email verification. \n\nFor the sign in directories, it is possible to brute force login attempts to either login portal, which could lead to account compromise.
CWE description: The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.
CVSS v3 base score: 9.8
Published: 2022-10-28T13:15Z
Repository: https://github.com/chatwoot/chatwoot
Language: Ruby
Files changed: config/initializers/rack_attack.rb
Commit date: 2022-09-02 23:09:03 +0530
Commit message: chore: Improve rack-attack configuration (#5389)

fixes: https://github.com/chatwoot/product/issues/540
Diff: diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index adefee312d..6c9c3754a1 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -22,10 +22,16 @@ class Rack::Attack
 
     def allowed_ip?
       allowed_ips = ['127.0.0.1', '::1']
       allowed_ips.include?(remote_ip)
     end
+
+    # Rails would allow requests to paths with extentions, so lets compare against the path with extention stripped
+    # example /auth & /auth.json would both work
+    def path_without_extentions
+      path[/^[^.]+/]
+    end
   end
 
   ### Throttle Spammy Clients ###
 
   # If any single client IP is making tons of requests, then they're
@@ -40,55 +46,66 @@ class Rack::Attack
   #
   # 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
-
-  ## Prevent Conversation Bombing on Widget APIs ###
-  throttle('api/v1/widget/conversations', limit: 6, period: 12.hours) do |req|
-    req.ip if req.path == '/api/v1/widget/conversations' && req.post?
+  ### Prevent Brute-Force Super Admin Login Attacks ###
+  throttle('super_admin_login/ip', limit: 5, period: 5.minutes) do |req|
+    req.ip if req.path_without_extentions == '/super_admin/sign_in' && req.post?
   end
 
-  ## Prevent Contact update Bombing in Widget API ###
-  throttle('api/v1/widget/contacts', limit: 60, period: 1.hour) do |req|
-    req.ip if req.path == '/api/v1/widget/contacts' && (req.patch? || req.put?)
-  end
-
-  # ref: https://github.com/rack/rack-attack/issues/399
-  throttle('login/email', limit: 20, period: 5.minutes) do |req|
-    if req.path == '/auth/sign_in' && req.post?
+  throttle('super_admin_login/email', limit: 5, period: 15.minutes) do |req|
+    if req.path_without_extentions == '/super_admin/sign_in' && req.post?
       # NOTE: This line used to throw ArgumentError /rails/action_mailbox/sendgrid/inbound_emails : invalid byte sequence in UTF-8
       # Hence placed in the if block
+      # ref: https://github.com/rack/rack-attack/issues/399
       email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
       email.to_s.downcase.gsub(/\s+/, '')
     end
   end
 
-  throttle('super_admin_login/email', limit: 20, period: 5.minutes) do |req|
-    if req.path == '/super_admin/sign_in' && req.post?
+  # ### Prevent Brute-Force Login Attacks ###
+  throttle('login/ip', limit: 5, period: 5.minutes) do |req|
+    req.ip if req.path_without_extentions == '/auth/sign_in' && req.post?
+  end
+
+  throttle('login/email', limit: 10, period: 15.minutes) do |req|
+    if req.path_without_extentions == '/auth/sign_in' && req.post?
+      # ref: https://github.com/rack/rack-attack/issues/399
       # NOTE: This line used to throw ArgumentError /rails/action_mailbox/sendgrid/inbound_emails : invalid byte sequence in UTF-8
       # Hence placed in the if block
       email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
       email.to_s.downcase.gsub(/\s+/, '')
     end
   end
 
+  ## Reset password throttling
+  throttle('reset_password/ip', limit: 5, period: 30.minutes) do |req|
+    req.ip if req.path_without_extentions == '/auth/password' && req.post?
+  end
+
   throttle('reset_password/email', limit: 5, period: 1.hour) do |req|
-    if req.path == '/auth/password' && req.post?
+    if req.path_without_extentions == '/auth/password' && req.post?
       email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
       email.to_s.downcase.gsub(/\s+/, '')
     end
   end
+
+  ## Prevent Brute-Force Signup Attacks ###
+  throttle('accounts/ip', limit: 5, period: 30.minutes) do |req|
+    req.ip if req.path_without_extentions == '/api/v1/accounts' && req.post?
+  end
+
+  ## Prevent Conversation Bombing on Widget APIs ###
+  throttle('api/v1/widget/conversations', limit: 6, period: 12.hours) do |req|
+    req.ip if req.path_without_extentions == '/api/v1/widget/conversations' && req.post?
+  end
+
+  ## Prevent Contact update Bombing in Widget API ###
+  throttle('api/v1/widget/contacts', limit: 60, period: 1.hour) do |req|
+    req.ip if req.path_without_extentions == '/api/v1/widget/contacts' && (req.patch? || req.put?)
+  end
 end
 
 # Log blocked events
 ActiveSupport::Notifications.subscribe('throttle.rack_attack') do |_name, _start, _finish, _request_id, payload|
   Rails.logger.warn "[Rack::Attack][Blocked] remote_ip: \"#{payload[:request].remote_ip}\", path: \"#{payload[:request].path}\""

Vulnerable code:   ### 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

  ## Prevent Conversation Bombing on Widget APIs ###
  throttle('api/v1/widget/conversations', limit: 6, period: 12.hours) do |req|
    req.ip if req.path == '/api/v1/widget/conversations' && req.post?
  ## Prevent Contact update Bombing in Widget API ###
  throttle('api/v1/widget/contacts', limit: 60, period: 1.hour) do |req|
    req.ip if req.path == '/api/v1/widget/contacts' && (req.patch? || req.put?)
  end

  # ref: https://github.com/rack/rack-attack/issues/399
  throttle('login/email', limit: 20, period: 5.minutes) do |req|
    if req.path == '/auth/sign_in' && req.post?
  throttle('super_admin_login/email', limit: 20, period: 5.minutes) do |req|
    if req.path == '/super_admin/sign_in' && req.post?
    if req.path == '/auth/password' && req.post?
Fixed code: 
    # Rails would allow requests to paths with extentions, so lets compare against the path with extention stripped
    # example /auth & /auth.json would both work
    def path_without_extentions
      path[/^[^.]+/]
    end
  ### Prevent Brute-Force Super Admin Login Attacks ###
  throttle('super_admin_login/ip', limit: 5, period: 5.minutes) do |req|
    req.ip if req.path_without_extentions == '/super_admin/sign_in' && req.post?
  throttle('super_admin_login/email', limit: 5, period: 15.minutes) do |req|
    if req.path_without_extentions == '/super_admin/sign_in' && req.post?
      # ref: https://github.com/rack/rack-attack/issues/399
  # ### Prevent Brute-Force Login Attacks ###
  throttle('login/ip', limit: 5, period: 5.minutes) do |req|
    req.ip if req.path_without_extentions == '/auth/sign_in' && req.post?
  end

  throttle('login/email', limit: 10, period: 15.minutes) do |req|
    if req.path_without_extentions == '/auth/sign_in' && req.post?
      # ref: https://github.com/rack/rack-attack/issues/399
  ## Reset password throttling
  throttle('reset_password/ip', limit: 5, period: 30.minutes) do |req|
    req.ip if req.path_without_extentions == '/auth/password' && req.post?
  end

    if req.path_without_extentions == '/auth/password' && req.post?

  ## Prevent Brute-Force Signup Attacks ###
  throttle('accounts/ip', limit: 5, period: 30.minutes) do |req|
    req.ip if req.path_without_extentions == '/api/v1/accounts' && req.post?
  end

  ## Prevent Conversation Bombing on Widget APIs ###
  throttle('api/v1/widget/conversations', limit: 6, period: 12.hours) do |req|
    req.ip if req.path_without_extentions == '/api/v1/widget/conversations' && req.post?
  end

  ## Prevent Contact update Bombing in Widget API ###
  throttle('api/v1/widget/contacts', limit: 60, period: 1.hour) do |req|
    req.ip if req.path_without_extentions == '/api/v1/widget/contacts' && (req.patch? || req.put?)
  end