CVE-2022-46177 (published 2023-01-05T20:15Z)

Discourse is an option source discussion platform. Prior to version 2.8.14 on the `stable` branch and version 3.0.0.beta16 on the `beta` and `tests-passed` branches, when a user requests for a password reset link email, then changes their primary email, the old reset email is still valid. When the old reset email is used to reset the password, the Discourse account's primary email would be re-linked to the old email. If the old email address is compromised or has transferred ownership, this leads to an account takeover. This is however mitigated by the SiteSetting `email_token_valid_hours` which is currently 48 hours. Users should upgrade to versions 2.8.14 or 3.0.0.beta15 to receive a patch. As a workaround, lower `email_token_valid_hours ` as needed.

CVSS v3 base score: 8.1, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N)

Weakness information:
cwe_id: CWE-613
cwe_name: Insufficient Session Expiration
description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Alan Guo Xiang Tan
author_date: 2023-01-05 06:08:55+08:00
msg: SECURITY: Delete email tokens when a user's email is changed or deleted (#19735)

Co-authored-by: OsamaSayegh <asooomaasoooma90@gmail.com>
repository: repo_url: https://github.com/discourse/discourse
repo_name: discourse/discourse
description: A platform for community discussion. Free, open, simple.
repo_language: Ruby
owner: discourse
file_changes: filename: user_email.rb
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,10 @@ class UserEmail < ActiveRecord::Base
 
   scope :secondary, -> { where(primary: false) }
 
+  before_save ->() { destroy_email_tokens(self.email_was) }, if: :will_save_change_to_email?
+
+  after_destroy { destroy_email_tokens(self.email) }
+
   def normalize_email
     self.normalized_email = if self.email.present?
       username, domain = self.email.split('@', 2)
@@ -62,6 +66,10 @@ class UserEmail < ActiveRecord::Base
       )
     end
   end
+
+  def destroy_email_tokens(email)
+    EmailToken.where(email: email).destroy_all
+  end
 end
 
 # == Schema Information

programming_language: Ruby
filename: users_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -3656,17 +3656,18 @@ RSpec.describe UsersController do
       expect(user1.email_change_requests).to contain_exactly(request_1)
     end
 
-    it "can destroy associated email tokens" do
+    it "destroys associated email tokens and email change requests" do
       new_email = 'new.n.cool@example.com'
       updater = EmailUpdater.new(guardian: user1.guardian, user: user1)
+      updater.change_to(new_email)
 
-      expect { updater.change_to(new_email) }
-        .to change { user1.email_tokens.count }.by(1)
+      email_token = updater.change_req.new_email_token
+      expect(email_token).to be_present
 
-      expect { delete "/u/#{user1.username}/preferences/email.json", params: { email: new_email } }
-        .to change { user1.email_tokens.count }.by(-1)
+      delete "/u/#{user1.username}/preferences/email.json", params: { email: new_email }
 
-      expect(user1.email_tokens.first.email).to eq(user1.email)
+      expect(EmailToken.find_by(id: email_token.id)).to eq(nil)
+      expect(EmailChangeRequest.find_by(id: updater.change_req.id)).to eq(nil)
     end
   end
 
@@ -3922,8 +3923,7 @@ RSpec.describe UsersController do
         expect(user.email).to eq('updatedemail@example.com')
         expect(user.email_tokens.where(email: 'updatedemail@example.com', expired: false)).to be_present
 
-        token.reload
-        expect(token.expired?).to eq(true)
+        expect(EmailToken.find_by(id: token.id)).to eq(nil)
       end
 
       it 'tells the user to slow down after many requests' do
@@ -4013,8 +4013,7 @@ RSpec.describe UsersController do
         expect(user.email).to eq('updatedemail@example.com')
         expect(user.email_tokens.where(email: 'updatedemail@example.com', expired: false)).to be_present
 
-        token.reload
-        expect(token.expired?).to eq(true)
+        expect(EmailToken.find_by(id: token.id)).to eq(nil)
       end
 
       it 'tells the user to slow down after many requests' do

programming_language: Ruby
filename: users_email_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -242,6 +242,28 @@ RSpec.describe UsersEmailController do
         end
       end
     end
+
+    it "destroys email tokens associated with the old email after the new email is confirmed" do
+      SiteSetting.enable_secondary_emails = true
+
+      email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset])
+
+      updater = EmailUpdater.new(guardian: user.guardian, user: user)
+      updater.change_to('bubblegum@adventuretime.ooo')
+
+      sign_in(user)
+      put "/u/confirm-new-email", params: {
+        token: "#{updater.change_req.new_email_token.token}"
+      }
+
+      new_password = SecureRandom.hex
+      put "/u/password-reset/#{email_token.token}.json", params: {
+        password: new_password
+      }
+      expect(response.parsed_body["success"]).to eq(false)
+      expect(response.parsed_body["message"]).to eq(I18n.t("password_reset.no_token", base_url: Discourse.base_url))
+      expect(user.reload.confirm_password?(new_password)).to eq(false)
+    end
   end
 
   describe '#confirm-old-email' do

programming_language: Ruby