Project

General

Profile

Patch #44578 » 0001-Invalidate-sessions-and-autologin-tokens-when-an-ema.patch

Jens Krämer, 2026-10-06 07:28

View differences:

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
    (1-1/1)