Skip to content

Commit

Permalink
SECURITY: Limit passwords to 200 characters
Browse files Browse the repository at this point in the history
Prevents layer 8 attack.
  • Loading branch information
riking authored and eviltrout committed Sep 12, 2014
1 parent 216ee9f commit 2c6d03f
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 7 deletions.
2 changes: 2 additions & 0 deletions app/controllers/session_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ def create
params.require(:login)
params.require(:password)

return invalid_credentials if params[:password].length > User.max_password_length

login = params[:login].strip
login = login[1..-1] if login[0] == "@"

Expand Down
22 changes: 16 additions & 6 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ def create
return
end

if params[:password] && params[:password].length > User.max_password_length
render json: { success: false, message: I18n.t("login.password_too_long") }
return
end

user = User.new(user_params)

authentication = UserAuthenticator.new(user, session)
Expand Down Expand Up @@ -221,12 +226,17 @@ def password_reset
if !@user
flash[:error] = I18n.t('password_reset.no_token')
elsif request.put?
raise Discourse::InvalidParameters.new(:password) unless params[:password].present?
@user.password = params[:password]
@user.password_required!
if @user.save
Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore
logon_after_password_reset
@invalid_password = params[:password].blank? || params[:password].length > User.max_password_length

if @invalid_password
@user.errors.add(:password, :invalid)
else
@user.password = params[:password]
@user.password_required!
if @user.save
Invite.invalidate_for_email(@user.email) # invite link can't be used to log in anymore
logon_after_password_reset
end
end
end
render layout: 'no_js'
Expand Down
5 changes: 5 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ module NewTopicDuration
LAST_VISIT = -2
end

def self.max_password_length
200
end

def self.username_length
SiteSetting.min_username_length.to_i..SiteSetting.max_username_length.to_i
end
Expand Down Expand Up @@ -679,6 +683,7 @@ def ensure_password_is_hashed
end

def hash_password(password, salt)
raise "password is too long" if password.size > User.max_password_length
Pbkdf2.hash_password(password, salt, Rails.configuration.pbkdf2_iterations, Rails.configuration.pbkdf2_algorithm)
end

Expand Down
2 changes: 1 addition & 1 deletion app/views/users/password_reset.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

<%=form_tag({}, method: :put) do %>
<p>
<input id="user_password" name="password" size="30" type="password">
<input id="user_password" name="password" size="30" type="password" maxlength="<%= User.max_password_length %>">
<label><%= t('js.user.password.instructions', count: SiteSetting.min_password_length) %></label>
</p>
<p>
Expand Down
1 change: 1 addition & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,7 @@ en:
omniauth_error: "Sorry, there was an error authorizing your %{strategy} account. Perhaps you did not approve authorization?"
omniauth_error_unknown: "Something went wrong processing your log in, please try again."
new_registrations_disabled: "New account registrations are not allowed at this time."
password_too_long: "Passwords are limited to 200 characters."

user:
username:
Expand Down
8 changes: 8 additions & 0 deletions spec/controllers/session_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ def get_sso(return_path)
end
end

describe 'invalid password' do
it "should return an error with an invalid password if too long" do
User.any_instance.expects(:confirm_password?).never
xhr :post, :create, login: user.username, password: ('s' * (User.max_password_length + 1))
::JSON.parse(response.body)['error'].should be_present
end
end

describe 'suspended user' do
it 'should return an error' do
User.any_instance.stubs(:suspended?).returns(true)
Expand Down
19 changes: 19 additions & 0 deletions spec/controllers/users_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,28 @@
EmailToken.expects(:confirm).with(token).returns(user)
end

it "fails when the password is blank" do
put :password_reset, token: token, password: ''
assigns(:user).errors.should be_present
session[:current_user_id].should be_blank
end

it "fails when the password is too long" do
put :password_reset, token: token, password: ('x' * (User.max_password_length + 1))
assigns(:user).errors.should be_present
session[:current_user_id].should be_blank
end

it "logs in the user" do
put :password_reset, token: token, password: 'newpassword'
assigns(:user).errors.should be_blank
session[:current_user_id].should be_present
end

it "doesn't log in the user when not approved" do
SiteSetting.expects(:must_approve_users?).returns(true)
put :password_reset, token: token, password: 'newpassword'
assigns(:user).errors.should be_blank
session[:current_user_id].should be_blank
end
end
Expand Down Expand Up @@ -508,6 +522,11 @@ def post_user
include_examples 'failed signup'
end

context 'when password is too long' do
let(:create_params) { {name: @user.name, username: @user.username, password: "x" * (User.max_password_length + 1), email: @user.email} }
include_examples 'failed signup'
end

context 'when password param is missing' do
let(:create_params) { {name: @user.name, username: @user.username, email: @user.email} }
include_examples 'failed signup'
Expand Down
26 changes: 26 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1150,4 +1150,30 @@
end
end

describe "hash_passwords" do

let(:too_long) { "x" * (User.max_password_length + 1) }

def hash(password, salt)
User.new.send(:hash_password, password, salt)
end

it "returns the same hash for the same password and salt" do
hash('poutine', 'gravy').should == hash('poutine', 'gravy')
end

it "returns a different hash for the same salt and different password" do
hash('poutine', 'gravy').should_not == hash('fries', 'gravy')
end

it "returns a different hash for the same password and different salt" do
hash('poutine', 'gravy').should_not == hash('poutine', 'cheese')
end

it "raises an error when passwords are too long" do
-> { hash(too_long, 'gravy') }.should raise_error
end

end

end

0 comments on commit 2c6d03f

Please sign in to comment.