FIX: rate limit password reset email

This commit is contained in:
Sam 2014-08-18 10:55:30 +10:00
parent 582ec5954f
commit e0a82d3088
5 changed files with 38 additions and 14 deletions

View File

@ -6,25 +6,40 @@ export default DiscourseController.extend(ModalFunctionality, {
// You need a value in the field to submit it. // You need a value in the field to submit it.
submitDisabled: function() { submitDisabled: function() {
return this.blank('accountEmailOrUsername'); return this.blank('accountEmailOrUsername') || this.get('disabled');
}.property('accountEmailOrUsername'), }.property('accountEmailOrUsername', 'disabled'),
actions: { actions: {
submit: function() { submit: function() {
if (!this.get('accountEmailOrUsername')) return false; var self = this;
Discourse.ajax("/session/forgot_password", { if (this.get('submitDisabled')) return false;
this.set('disabled', true);
var success = function() {
// don't tell people what happened, this keeps it more secure (ensure same on server)
var escaped = Handlebars.Utils.escapeExpression(self.get('accountEmailOrUsername'));
if (self.get('accountEmailOrUsername').match(/@/)) {
self.flash(I18n.t('forgot_password.complete_email', {email: escaped}));
} else {
self.flash(I18n.t('forgot_password.complete_username', {username: escaped}));
}
};
var fail = function(e) {
self.flash(e.responseJSON.errors[0], 'alert-error');
};
Discourse.ajax('/session/forgot_password', {
data: { login: this.get('accountEmailOrUsername') }, data: { login: this.get('accountEmailOrUsername') },
type: 'POST' type: 'POST'
}).then(success, fail).finally(function(){
setTimeout(function(){
self.set('disabled',false);
}, 10*1000);
}); });
// don't tell people what happened, this keeps it more secure (ensure same on server)
var escaped = Handlebars.Utils.escapeExpression(this.get('accountEmailOrUsername'));
if (this.get('accountEmailOrUsername').match(/@/)) {
this.flash(I18n.t('forgot_password.complete_email', {email: escaped}));
} else {
this.flash(I18n.t('forgot_password.complete_username', {username: escaped}));
}
return false; return false;
} }
} }

View File

@ -1,3 +1,5 @@
require_dependency 'rate_limiter'
class SessionController < ApplicationController class SessionController < ApplicationController
skip_before_filter :redirect_to_login_if_required skip_before_filter :redirect_to_login_if_required
@ -93,6 +95,9 @@ class SessionController < ApplicationController
return return
end end
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!
user = User.find_by_username_or_email(params[:login]) user = User.find_by_username_or_email(params[:login])
if user.present? if user.present?
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)
@ -100,6 +105,9 @@ class SessionController < ApplicationController
end end
# always render of so we don't leak information # always render of so we don't leak information
render json: {result: "ok"} render json: {result: "ok"}
rescue RateLimiter::LimitExceeded
render_json_error(I18n.t("rate_limiter.slow_down"))
end end
def current def current

View File

@ -292,6 +292,7 @@ en:
rate_limiter: rate_limiter:
slow_down: "You have performed this action too many times, try again later"
too_many_requests: "We have a daily limit on how many times that action can be taken. Please wait %{time_left} before trying again." too_many_requests: "We have a daily limit on how many times that action can be taken. Please wait %{time_left} before trying again."
hours: hours:
one: "1 hour" one: "1 hour"

View File

@ -19,7 +19,7 @@ class RateLimiter
def initialize(user, key, max, secs) def initialize(user, key, max, secs)
@user = user @user = user
@key = "l-rate-limit:#{@user.id}:#{key}" @key = "l-rate-limit:#{@user && @user.id}:#{key}"
@max = max @max = max
@secs = secs @secs = secs
end end
@ -71,6 +71,6 @@ class RateLimiter
end end
def rate_unlimited? def rate_unlimited?
!!(RateLimiter.disabled? || @user.staff?) !!(RateLimiter.disabled? || (@user && @user.staff?))
end end
end end

View File

@ -360,7 +360,7 @@ describe SessionController do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
it "returns a 500 if local logins are disabled" do it "returns a 500 if local logins are disabled" do
SiteSetting.stubs(:enable_local_logins).returns(false) SiteSetting.enable_local_logins = false
xhr :post, :forgot_password, login: user.username xhr :post, :forgot_password, login: user.username
response.code.to_i.should == 500 response.code.to_i.should == 500
end end