fix(user): verify the authenticated account's password on email change

The password was checked through the login lookup by username or email, which could resolve to a different account, and the email of that account was then changed. Load the account by its ID and check its own password, keeping the re-authentication throttle and the lockout counter.
This commit is contained in:
kolaente
2026-11-03 22:03:18 +01:00
parent 02ed6b293f
commit 28fa8b1525
2 changed files with 47 additions and 4 deletions
+14 -4
View File
@@ -50,15 +50,25 @@ func EmailUpdateMessage() string {
// (kicking off confirmation when the mailer is enabled). Shared by the v1 and
// v2 email-update handlers; only HTTP input binding stays in the handlers.
func ChangeUserEmail(ctx context.Context, s *xorm.Session, u *User, password, newEmail string) error {
var verified *User
err := ThrottleReauth(u.ID, func() (err error) {
verified, err = CheckUserCredentials(ctx, s, &Login{Username: u.Username, Password: password})
if password == "" {
return ErrNoUsernamePassword{}
}
target, err := GetUserByID(s, u.ID)
if err != nil {
return err
}
if !target.IsLocalUser() {
return &ErrAccountIsNotLocal{UserID: target.ID}
}
err = ThrottleReauth(target.ID, func() error {
return CheckUserPasswordCountingFailures(ctx, s, target, password)
})
if err != nil {
return err
}
return UpdateEmail(s, &EmailUpdate{User: verified, NewEmail: newEmail})
return UpdateEmail(s, &EmailUpdate{User: target, NewEmail: newEmail})
}
// UpdateEmail stages the address as PendingEmail until confirmed; with the mailer off it applies immediately.
+33
View File
@@ -310,3 +310,36 @@ func TestResendEmailConfirmation(t *testing.T) {
assert.Equal(t, before[0].Token, after[0].Token)
})
}
func TestChangeUserEmail(t *testing.T) {
t.Run("verifies the password of the authenticated account", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
require.NoError(t, UpdateUserPassword(s, &User{ID: 2}, "user2password"))
// The username points at another account, e.g. a stale token claim.
err := ChangeUserEmail(context.Background(), s, &User{ID: 1, Username: "user2"}, "user2password", "new1@example.com")
require.Error(t, err)
assert.True(t, IsErrWrongUsernameOrPassword(err))
s2 := db.NewSession()
defer s2.Close()
updated, err := GetUserWithEmail(s2, &User{ID: 1})
require.NoError(t, err)
assert.Equal(t, "user1@example.com", updated.Email)
assert.Empty(t, updated.PendingEmail)
})
t.Run("changes the email", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
require.NoError(t, ChangeUserEmail(context.Background(), s, &User{ID: 1}, "12345678", "new1@example.com"))
updated, err := GetUserWithEmail(s, &User{ID: 1})
require.NoError(t, err)
assert.Equal(t, "new1@example.com", updated.Email)
})
}