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:
parent
78c775c39e
commit
f39ae8a903
|
@ -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:)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue