SECURITY: Rate limit MFA by login if possible (#11938)

This ensures we rate limit on logins where possible, we also normalize logins for the rate limiters centrally.
This commit is contained in:
Robin Ward 2021-02-02 18:26:28 -05:00 committed by Sam Saffron
parent d8627cf43f
commit 98f775506d
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
2 changed files with 46 additions and 10 deletions

View File

@ -277,10 +277,7 @@ class SessionController < ApplicationController
return invalid_credentials if params[:password].length > User.max_password_length return invalid_credentials if params[:password].length > User.max_password_length
login = params[:login].strip if user = User.find_by_username_or_email(normalized_login_param)
login = login[1..-1] if login[0] == "@"
if user = User.find_by_username_or_email(login)
# If their password is correct # If their password is correct
unless user.confirm_password?(params[:password]) unless user.confirm_password?(params[:password])
@ -408,12 +405,12 @@ class SessionController < ApplicationController
RateLimiter.new(nil, "forgot-password-hr-#{request.remote_ip}", 6, 1.hour).performed! RateLimiter.new(nil, "forgot-password-hr-#{request.remote_ip}", 6, 1.hour).performed!
RateLimiter.new(nil, "forgot-password-min-#{request.remote_ip}", 3, 1.minute).performed! RateLimiter.new(nil, "forgot-password-min-#{request.remote_ip}", 3, 1.minute).performed!
user = User.find_by_username_or_email(params[:login]) user = User.find_by_username_or_email(normalized_login_param)
if user if user
RateLimiter.new(nil, "forgot-password-login-day-#{user.username}", 6, 1.day).performed! RateLimiter.new(nil, "forgot-password-login-day-#{user.username}", 6, 1.day).performed!
else else
RateLimiter.new(nil, "forgot-password-login-hour-#{params[:login].to_s[0..100]}", 5, 1.hour).performed! RateLimiter.new(nil, "forgot-password-login-hour-#{normalized_login_param}", 5, 1.hour).performed!
end end
user_presence = user.present? && user.human? && !user.staged user_presence = user.present? && user.human? && !user.staged
@ -482,6 +479,16 @@ class SessionController < ApplicationController
protected protected
def normalized_login_param
login = params[:login].to_s
if login.present?
login = login[1..-1] if login[0] == "@"
User.normalize_username(login.strip)[0..100]
else
nil
end
end
def check_local_login_allowed(user: nil, check_login_via_email: false) def check_local_login_allowed(user: nil, check_login_via_email: false)
# admin-login can get around enabled SSO/disabled local logins # admin-login can get around enabled SSO/disabled local logins
return if user&.admin? return if user&.admin?
@ -595,6 +602,9 @@ class SessionController < ApplicationController
def rate_limit_second_factor_totp def rate_limit_second_factor_totp
return if params[:second_factor_token].blank? return if params[:second_factor_token].blank?
RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed!
if normalized_login_param.present?
RateLimiter.new(nil, "second-factor-min-#{normalized_login_param}", 3, 1.minute).performed!
end
end end
def render_sso_error(status:, text:) def render_sso_error(status:, text:)

View File

@ -1731,17 +1731,16 @@ RSpec.describe SessionController do
expect(json["error_type"]).to eq("rate_limit") expect(json["error_type"]).to eq("rate_limit")
end end
it 'rate limits second factor attempts' do it 'rate limits second factor attempts by IP' do
RateLimiter.enable RateLimiter.enable
RateLimiter.clear_all! RateLimiter.clear_all!
3.times do 3.times do |x|
post "/session.json", params: { post "/session.json", params: {
login: user.username, login: "#{user.username}#{x}",
password: 'myawesomepassword', password: 'myawesomepassword',
second_factor_token: '000000' second_factor_token: '000000'
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end
@ -1755,6 +1754,33 @@ RSpec.describe SessionController do
json = response.parsed_body json = response.parsed_body
expect(json["error_type"]).to eq("rate_limit") expect(json["error_type"]).to eq("rate_limit")
end end
it 'rate limits second factor attempts by login' do
RateLimiter.enable
RateLimiter.clear_all!
3.times do |x|
post "/session.json", params: {
login: user.username,
password: 'myawesomepassword',
second_factor_token: '000000'
}, env: { "REMOTE_ADDR": "1.2.3.#{x}" }
expect(response.status).to eq(200)
end
[user.username + " ", user.username.capitalize, user.username].each_with_index do |username , x|
post "/session.json", params: {
login: username,
password: 'myawesomepassword',
second_factor_token: '000000'
}, env: { "REMOTE_ADDR": "1.2.4.#{x}" }
expect(response.status).to eq(429)
json = response.parsed_body
expect(json["error_type"]).to eq("rate_limit")
end
end
end end
end end