From 130bbb21aa292c8063cdfdf15ac1e638edb9583b Mon Sep 17 00:00:00 2001
From: Jens Kraemer <jk@jkraemer.net>
Date: Mon, 26 Oct 2026 12:19:04 +0800
Subject: [PATCH] Reject the current password on lost password reset when it
 has expired

The forced password change after login rejects the current password both
when must_change_passwd is set and when the password has expired, but the
lost password reset only checked must_change_passwd. Users with an
expired password could set it again via the lost password form.
---
 app/controllers/account_controller.rb      |  2 +-
 test/functional/account_controller_test.rb | 24 ++++++++++++++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
index dd482bc56..66486e23b 100644
--- a/app/controllers/account_controller.rb
+++ b/app/controllers/account_controller.rb
@@ -82,7 +82,7 @@ class AccountController < ApplicationController
         return
       end
       if request.post?
-        if @user.must_change_passwd? && @user.check_password?(params[:new_password])
+        if @user.must_change_password? && @user.check_password?(params[:new_password])
           flash.now[:error] = l(:notice_new_password_must_be_different)
         else
           @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb
index e7c04bafb..c61f79dea 100644
--- a/test/functional/account_controller_test.rb
+++ b/test/functional/account_controller_test.rb
@@ -640,6 +640,30 @@ class AccountControllerTest < Redmine::ControllerTest
     assert_select 'input[type=hidden][name=token][value=?]', token.value
   end
 
+  def test_post_lost_password_with_token_should_not_accept_same_password_if_password_expired
+    user = User.find(2)
+    user.password = "originalpassword"
+    user.save!
+    user.update_column(:passwd_changed_on, 14.days.ago)
+    token = Token.create!(:action => 'recovery', :user => user)
+    with_settings :password_max_age => 7 do
+      post(
+        :lost_password,
+        :params => {
+          :token => token.value,
+          :new_password => 'originalpassword',
+          :new_password_confirmation => 'originalpassword'
+        }
+      )
+      assert user.reload.must_change_password?, "Password expiry was reset"
+    end
+    assert_response :success
+    assert_not_nil Token.find_by_id(token.id), "Token was deleted"
+
+    assert_select '.flash', :text => /The new password must be different/
+    assert_select 'input[type=hidden][name=token][value=?]', token.value
+  end
+
   def test_post_lost_password_with_token_should_reset_must_change_password
     user = User.find(2)
     user.password = "originalpassword"
-- 
2.56.0

