Patch #44578 » 0001-Invalidate-sessions-and-autologin-tokens-when-an-ema.patch
| app/controllers/application_controller.rb | ||
|---|---|---|
| 112 | 112 |
end |
| 113 | 113 |
end |
| 114 | 114 | |
| 115 |
# Issues a new session token after the session tokens of the current user |
|
| 116 |
# were deleted, so that the current session stays valid. |
|
| 117 |
# API requests do not use the session and thus do not need a token. |
|
| 118 |
def renew_session_token |
|
| 119 |
session[:tk] = User.current.generate_session_token unless api_request? |
|
| 120 |
end |
|
| 121 | ||
| 115 | 122 |
def user_setup |
| 116 | 123 |
# Check the settings cache for each request |
| 117 | 124 |
Setting.check_cache |
| app/controllers/email_addresses_controller.rb | ||
|---|---|---|
| 76 | 76 | |
| 77 | 77 |
def destroy |
| 78 | 78 |
@address.destroy |
| 79 |
if @user == User.current |
|
| 80 |
# The session token was destroyed by the address deletion, generate a new one |
|
| 81 |
renew_session_token |
|
| 82 |
end |
|
| 79 | 83 | |
| 80 | 84 |
respond_to do |format| |
| 81 | 85 |
format.html do |
| app/controllers/my_controller.rb | ||
|---|---|---|
| 55 | 55 |
if request.put? |
| 56 | 56 |
@user.safe_attributes = params[:user] |
| 57 | 57 |
@user.pref.safe_attributes = params[:pref] |
| 58 |
mail_changed = @user.mail_changed? |
|
| 58 | 59 |
if @user.save |
| 59 | 60 |
@user.pref.save |
| 61 |
if mail_changed |
|
| 62 |
# The session token was destroyed by the email address change, generate a new one |
|
| 63 |
renew_session_token |
|
| 64 |
end |
|
| 60 | 65 |
respond_to do |format| |
| 61 | 66 |
format.html do |
| 62 | 67 |
flash[:notice] = l(:notice_account_updated) |
| ... | ... | |
| 110 | 115 |
@user.must_change_passwd = false |
| 111 | 116 |
if @user.save |
| 112 | 117 |
# The session token was destroyed by the password change, generate a new one |
| 113 |
session[:tk] = @user.generate_session_token
|
|
| 118 |
renew_session_token
|
|
| 114 | 119 |
Mailer.deliver_password_updated(@user, User.current) |
| 115 | 120 |
flash[:notice] = l(:notice_account_password_updated) |
| 116 | 121 |
redirect_to my_account_path |
| app/controllers/twofa_controller.rb | ||
|---|---|---|
| 53 | 53 |
def activate |
| 54 | 54 |
if @twofa.confirm_pairing!(params[:twofa_code].to_s) |
| 55 | 55 |
# The session token was destroyed by the twofa pairing, generate a new one |
| 56 |
session[:tk] = @user.generate_session_token
|
|
| 56 |
renew_session_token
|
|
| 57 | 57 |
flash[:notice] = l('twofa_activated', bc_path: my_twofa_backup_codes_init_path)
|
| 58 | 58 |
redirect_to my_account_path |
| 59 | 59 |
else |
| app/controllers/users_controller.rb | ||
|---|---|---|
| 186 | 186 |
@user.safe_attributes = params[:user] |
| 187 | 187 |
# Was the account actived ? (do it before User#save clears the change) |
| 188 | 188 |
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE]) |
| 189 |
mail_changed = @user.mail_changed? |
|
| 189 | 190 |
# TODO: Similar to My#account |
| 190 | 191 |
@user.pref.safe_attributes = params[:pref] |
| 191 | 192 | |
| 192 | 193 |
if @user.save |
| 193 | 194 |
@user.pref.save |
| 194 | 195 | |
| 196 |
if mail_changed && @user == User.current |
|
| 197 |
# The session token was destroyed by the email address change, generate a new one |
|
| 198 |
renew_session_token |
|
| 199 |
end |
|
| 200 | ||
| 195 | 201 |
Mailer.deliver_password_updated(@user, User.current) if is_updating_password |
| 196 | 202 |
if was_activated |
| 197 | 203 |
Mailer.deliver_account_activated(@user) |
| app/models/email_address.rb | ||
|---|---|---|
| 139 | 139 |
end |
| 140 | 140 | |
| 141 | 141 |
# Delete all outstanding password reset tokens on email change. |
| 142 |
# Delete the autologin and session tokens as well to prohibit session leakage. |
|
| 142 | 143 |
# This helps to keep the account secure in case the associated email account |
| 143 | 144 |
# was compromised. |
| 144 | 145 |
def destroy_tokens |
| 145 | 146 |
if saved_change_to_address? || destroyed? |
| 146 |
tokens = ['recovery'] |
|
| 147 |
Token.where(:user_id => user_id, :action => tokens).delete_all |
|
| 147 |
user&.delete_login_tokens |
|
| 148 | 148 |
end |
| 149 | 149 |
end |
| 150 | 150 | |
| app/models/user.rb | ||
|---|---|---|
| 481 | 481 |
Token.where(:user_id => id, :action => 'autologin', :value => value).delete_all |
| 482 | 482 |
end |
| 483 | 483 | |
| 484 |
# Deletes all tokens that could be used to log in |
|
| 485 |
def delete_login_tokens |
|
| 486 |
Token.where(:user_id => id, :action => ['recovery', 'autologin', 'session']).delete_all |
|
| 487 |
end |
|
| 488 | ||
| 484 | 489 |
def twofa_totp_key |
| 485 | 490 |
read_ciphered_attribute(:twofa_totp_key) |
| 486 | 491 |
end |
| ... | ... | |
| 983 | 988 |
# was compromised. |
| 984 | 989 |
def destroy_tokens |
| 985 | 990 |
if saved_change_to_hashed_password? || (saved_change_to_status? && !active?) || (saved_change_to_twofa_scheme? && twofa_scheme.present?) |
| 986 |
tokens = ['recovery', 'autologin', 'session'] |
|
| 987 |
Token.where(:user_id => id, :action => tokens).delete_all |
|
| 991 |
delete_login_tokens |
|
| 988 | 992 |
end |
| 989 | 993 |
end |
| 990 | 994 | |
| test/integration/api_test/my_test.rb | ||
|---|---|---|
| 101 | 101 |
assert_kind_of Array, json['errors'] |
| 102 | 102 |
end |
| 103 | 103 | |
| 104 |
test "PUT /my/account.json with a changed mail should not generate a session token" do |
|
| 105 |
Token.create!(:user_id => 3, :action => 'session') |
|
| 106 | ||
| 107 |
put( |
|
| 108 |
'/my/account.json', |
|
| 109 |
:params => {:user => {:mail => 'dave@somenet.foo'}},
|
|
| 110 |
:headers => credentials('dlopper', 'foo'))
|
|
| 111 |
assert_response :no_content |
|
| 112 | ||
| 113 |
assert_equal 'dave@somenet.foo', User.find(3).mail |
|
| 114 |
assert_empty Token.where(:user_id => 3, :action => 'session') |
|
| 115 |
end |
|
| 116 | ||
| 104 | 117 |
test "GET /my/account.json authenticated via OAuth should not disclose the api_key" do |
| 105 | 118 |
application = Doorkeeper::Application.create!( |
| 106 | 119 |
:name => 'Test App', |
| test/integration/api_test/users_test.rb | ||
|---|---|---|
| 488 | 488 |
assert_equal '', @response.body |
| 489 | 489 |
end |
| 490 | 490 | |
| 491 |
test "PUT /users/:id.json by an admin changing their own mail should not generate a session token" do |
|
| 492 |
Token.create!(:user_id => 1, :action => 'session') |
|
| 493 | ||
| 494 |
put( |
|
| 495 |
'/users/1.json', |
|
| 496 |
:params => {:user => {:mail => 'newadmin@somenet.foo'}},
|
|
| 497 |
:headers => credentials('admin'))
|
|
| 498 |
assert_response :no_content |
|
| 499 | ||
| 500 |
assert_equal 'newadmin@somenet.foo', User.find(1).mail |
|
| 501 |
assert_empty Token.where(:user_id => 1, :action => 'session') |
|
| 502 |
end |
|
| 503 | ||
| 491 | 504 |
test "PUT /users/:id.xml with invalid parameters" do |
| 492 | 505 |
assert_no_difference('User.count') do
|
| 493 | 506 |
put( |
| test/integration/sessions_test.rb | ||
|---|---|---|
| 63 | 63 |
assert_response :ok |
| 64 | 64 |
end |
| 65 | 65 | |
| 66 |
def test_change_password_generates_a_new_token_for_current_session |
|
| 66 |
def test_change_password_kills_all_sessions_and_generates_a_new_token_for_current_session |
|
| 67 |
other_session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 67 | 68 |
log_user('jsmith', 'jsmith')
|
| 68 | 69 |
assert_not_nil token = session[:tk] |
| 69 | 70 | |
| ... | ... | |
| 79 | 80 |
) |
| 80 | 81 |
assert_response :found |
| 81 | 82 |
assert_not_equal token, session[:tk] |
| 83 |
assert_nil Token.find_by(:user_id => 2, :action => 'session', :value => token) |
|
| 84 |
assert_nil Token.find_by_id(other_session_token.id) |
|
| 85 | ||
| 86 |
get '/my/account' |
|
| 87 |
assert_response :ok |
|
| 88 |
end |
|
| 89 | ||
| 90 |
def test_change_mail_kills_sessions |
|
| 91 |
log_user('jsmith', 'jsmith')
|
|
| 92 | ||
| 93 |
jsmith = User.find(2) |
|
| 94 |
jsmith.mail = 'anotheraddress@somenet.foo' |
|
| 95 |
jsmith.save! |
|
| 96 | ||
| 97 |
get '/my/account' |
|
| 98 |
assert_response :found |
|
| 99 |
assert_includes flash[:error], 'Your session has expired' |
|
| 100 |
end |
|
| 101 | ||
| 102 |
def test_change_mail_kills_all_sessions_and_generates_a_new_token_for_current_session |
|
| 103 |
other_session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 104 |
log_user('jsmith', 'jsmith')
|
|
| 105 |
assert_not_nil token = session[:tk] |
|
| 106 | ||
| 107 |
put '/my/account', :params => {:user => {:mail => 'anotheraddress@somenet.foo'}}
|
|
| 108 |
assert_response :found |
|
| 109 |
assert_not_equal token, session[:tk] |
|
| 110 |
assert_nil Token.find_by(:user_id => 2, :action => 'session', :value => token) |
|
| 111 |
assert_nil Token.find_by_id(other_session_token.id) |
|
| 112 | ||
| 113 |
get '/my/account' |
|
| 114 |
assert_response :ok |
|
| 115 |
end |
|
| 116 | ||
| 117 |
def test_destroy_email_address_kills_all_sessions_and_generates_a_new_token_for_current_session |
|
| 118 |
email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
| 119 |
other_session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 120 | ||
| 121 |
log_user('jsmith', 'jsmith')
|
|
| 122 |
assert_not_nil token = session[:tk] |
|
| 123 | ||
| 124 |
delete "/users/2/email_addresses/#{email.id}"
|
|
| 125 |
assert_response :found |
|
| 126 |
assert_not_equal token, session[:tk] |
|
| 127 |
assert_nil Token.find_by(:user_id => 2, :action => 'session', :value => token) |
|
| 128 |
assert_nil Token.find_by_id(other_session_token.id) |
|
| 129 | ||
| 130 |
get '/my/account' |
|
| 131 |
assert_response :ok |
|
| 132 |
end |
|
| 133 | ||
| 134 |
def test_admin_destroying_other_users_email_address_should_not_touch_admin_session |
|
| 135 |
email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
| 136 |
jsmith_session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 137 | ||
| 138 |
log_user('admin', 'admin')
|
|
| 139 |
assert_not_nil token = session[:tk] |
|
| 140 | ||
| 141 |
delete "/users/2/email_addresses/#{email.id}"
|
|
| 142 |
assert_response :found |
|
| 143 |
assert_equal token, session[:tk] |
|
| 144 |
assert_nil Token.find_by_id(jsmith_session_token.id) |
|
| 145 | ||
| 146 |
get '/my/account' |
|
| 147 |
assert_response :ok |
|
| 148 |
end |
|
| 149 | ||
| 150 |
def test_admin_changing_own_mail_kills_all_sessions_and_generates_a_new_token_for_current_session |
|
| 151 |
other_session_token = Token.create!(:user_id => 1, :action => 'session') |
|
| 152 |
log_user('admin', 'admin')
|
|
| 153 |
assert_not_nil token = session[:tk] |
|
| 154 | ||
| 155 |
put '/users/1', :params => {:user => {:mail => 'newadmin@somenet.foo'}}
|
|
| 156 |
assert_response :found |
|
| 157 |
assert_not_equal token, session[:tk] |
|
| 158 |
assert_nil Token.find_by(:user_id => 1, :action => 'session', :value => token) |
|
| 159 |
assert_nil Token.find_by_id(other_session_token.id) |
|
| 160 | ||
| 161 |
get '/my/account' |
|
| 162 |
assert_response :ok |
|
| 163 |
end |
|
| 164 | ||
| 165 |
def test_admin_changing_other_users_mail_should_kill_their_sessions_and_keep_admin_session |
|
| 166 |
jsmith_session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 167 | ||
| 168 |
log_user('admin', 'admin')
|
|
| 169 |
assert_not_nil token = session[:tk] |
|
| 170 | ||
| 171 |
put '/users/2', :params => {:user => {:mail => 'anotheraddress@somenet.foo'}}
|
|
| 172 |
assert_response :found |
|
| 173 |
assert_equal token, session[:tk] |
|
| 174 |
assert_nil Token.find_by_id(jsmith_session_token.id) |
|
| 82 | 175 | |
| 83 | 176 |
get '/my/account' |
| 84 | 177 |
assert_response :ok |
| test/integration/twofa_test.rb | ||
|---|---|---|
| 273 | 273 |
def test_enable_twofa_should_destroy_tokens |
| 274 | 274 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
| 275 | 275 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
| 276 |
other_session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 276 | 277 | |
| 277 | 278 |
with_settings twofa: "2" do |
| 278 | 279 |
log_user('jsmith', 'jsmith')
|
| 280 |
assert_not_nil token = session[:tk] |
|
| 279 | 281 |
follow_redirect! |
| 280 | 282 |
assert_redirected_to "/my/twofa/totp/activate/confirm" |
| 281 | 283 |
follow_redirect! |
| ... | ... | |
| 290 | 292 | |
| 291 | 293 |
post "/my/twofa/totp/activate", params: {twofa_code: totp.now}
|
| 292 | 294 |
assert_redirected_to "/my/account" |
| 295 |
assert_not_equal token, session[:tk] |
|
| 296 |
assert_nil Token.find_by(:user_id => 2, :action => 'session', :value => token) |
|
| 297 |
assert User.verify_session_token(2, session[:tk]) |
|
| 293 | 298 |
end |
| 294 | 299 | |
| 295 | 300 |
assert_nil Token.find_by_id(recovery_token.id) |
| 296 | 301 |
assert_nil Token.find_by_id(autologin_token.id) |
| 302 |
assert_nil Token.find_by_id(other_session_token.id) |
|
| 297 | 303 |
end |
| 298 | 304 |
end |
| test/unit/email_address_test.rb | ||
|---|---|---|
| 24 | 24 |
User.current = nil |
| 25 | 25 |
end |
| 26 | 26 | |
| 27 |
def test_destroy_should_destroy_tokens |
|
| 28 |
email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
| 29 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
|
| 30 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
|
| 31 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 32 | ||
| 33 |
assert email.destroy |
|
| 34 | ||
| 35 |
assert_nil Token.find_by_id(recovery_token.id) |
|
| 36 |
assert_nil Token.find_by_id(autologin_token.id) |
|
| 37 |
assert_nil Token.find_by_id(session_token.id) |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def test_create_should_not_destroy_tokens |
|
| 41 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
|
| 42 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 43 | ||
| 44 |
EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
| 45 | ||
| 46 |
assert_equal autologin_token, Token.find_by_id(autologin_token.id) |
|
| 47 |
assert_equal session_token, Token.find_by_id(session_token.id) |
|
| 48 |
end |
|
| 49 | ||
| 50 |
def test_notify_change_should_not_destroy_tokens |
|
| 51 |
email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
| 52 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
|
| 53 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
|
| 54 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 55 | ||
| 56 |
email.notify = false |
|
| 57 |
assert email.save |
|
| 58 | ||
| 59 |
assert_equal recovery_token, Token.find_by_id(recovery_token.id) |
|
| 60 |
assert_equal autologin_token, Token.find_by_id(autologin_token.id) |
|
| 61 |
assert_equal session_token, Token.find_by_id(session_token.id) |
|
| 62 |
end |
|
| 63 | ||
| 64 |
def test_destroy_address_of_missing_user_should_succeed |
|
| 65 |
email = EmailAddress.create!(:user_id => 2, :address => 'another@somenet.foo') |
|
| 66 |
EmailAddress.where(:id => email.id).update_all(:user_id => 999) |
|
| 67 |
email.reload |
|
| 68 | ||
| 69 |
assert email.destroy |
|
| 70 |
assert_nil EmailAddress.find_by_id(email.id) |
|
| 71 |
end |
|
| 72 | ||
| 27 | 73 |
def test_address_with_punycode_tld_should_be_valid |
| 28 | 74 |
email = EmailAddress.new(address: 'jsmith@example.xn--80akhbyknj4f') |
| 29 | 75 |
assert email.valid? |
| test/unit/user_test.rb | ||
|---|---|---|
| 507 | 507 |
def test_password_change_should_destroy_tokens |
| 508 | 508 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
| 509 | 509 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
| 510 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 510 | 511 | |
| 511 | 512 |
user = User.find(2) |
| 512 | 513 |
user.password, user.password_confirmation = "a new password", "a new password" |
| ... | ... | |
| 514 | 515 | |
| 515 | 516 |
assert_nil Token.find_by_id(recovery_token.id) |
| 516 | 517 |
assert_nil Token.find_by_id(autologin_token.id) |
| 518 |
assert_nil Token.find_by_id(session_token.id) |
|
| 517 | 519 |
end |
| 518 | 520 | |
| 519 | 521 |
def test_mail_change_should_destroy_tokens |
| 520 | 522 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
| 521 | 523 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
| 524 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 522 | 525 | |
| 523 | 526 |
user = User.find(2) |
| 524 | 527 |
user.mail = "user@somwehere.com" |
| 525 | 528 |
assert user.save |
| 526 | 529 | |
| 527 | 530 |
assert_nil Token.find_by_id(recovery_token.id) |
| 528 |
assert_equal autologin_token, Token.find_by_id(autologin_token.id) |
|
| 531 |
assert_nil Token.find_by_id(autologin_token.id) |
|
| 532 |
assert_nil Token.find_by_id(session_token.id) |
|
| 533 |
end |
|
| 534 | ||
| 535 |
def test_lock_should_destroy_tokens |
|
| 536 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
|
| 537 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
|
| 538 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 539 | ||
| 540 |
user = User.find(2) |
|
| 541 |
user.status = User::STATUS_LOCKED |
|
| 542 |
assert user.save |
|
| 543 | ||
| 544 |
assert_nil Token.find_by_id(recovery_token.id) |
|
| 545 |
assert_nil Token.find_by_id(autologin_token.id) |
|
| 546 |
assert_nil Token.find_by_id(session_token.id) |
|
| 547 |
end |
|
| 548 | ||
| 549 |
def test_twofa_activation_should_destroy_tokens |
|
| 550 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
|
| 551 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
|
| 552 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 553 | ||
| 554 |
user = User.find(2) |
|
| 555 |
user.twofa_scheme = 'totp' |
|
| 556 |
assert user.save |
|
| 557 | ||
| 558 |
assert_nil Token.find_by_id(recovery_token.id) |
|
| 559 |
assert_nil Token.find_by_id(autologin_token.id) |
|
| 560 |
assert_nil Token.find_by_id(session_token.id) |
|
| 529 | 561 |
end |
| 530 | 562 | |
| 531 | 563 |
def test_change_on_other_fields_should_not_destroy_tokens |
| ... | ... | |
| 540 | 572 |
assert_equal autologin_token, Token.find_by_id(autologin_token.id) |
| 541 | 573 |
end |
| 542 | 574 | |
| 575 |
def test_delete_login_tokens_should_delete_recovery_autologin_and_session_tokens |
|
| 576 |
recovery_token = Token.create!(:user_id => 2, :action => 'recovery') |
|
| 577 |
autologin_token = Token.create!(:user_id => 2, :action => 'autologin') |
|
| 578 |
session_token = Token.create!(:user_id => 2, :action => 'session') |
|
| 579 |
api_token = Token.create!(:user_id => 2, :action => 'api') |
|
| 580 |
other_users_token = Token.create!(:user_id => 3, :action => 'session') |
|
| 581 | ||
| 582 |
User.find(2).delete_login_tokens |
|
| 583 | ||
| 584 |
assert_nil Token.find_by_id(recovery_token.id) |
|
| 585 |
assert_nil Token.find_by_id(autologin_token.id) |
|
| 586 |
assert_nil Token.find_by_id(session_token.id) |
|
| 587 |
assert Token.find_by_id(api_token.id) |
|
| 588 |
assert Token.find_by_id(other_users_token.id) |
|
| 589 |
end |
|
| 590 | ||
| 543 | 591 |
def test_validate_login_presence |
| 544 | 592 |
@admin.login = "" |
| 545 | 593 |
assert !@admin.save |