fix: purge authentication data on hard user deletion (#6168)

* fix: purge authentication data on hard user deletion

* fix: fail closed when 2FA status lookup fails

* fix: reject stale Telegram login callbacks

* fix(twofa): prevent concurrent backup code and lockout bypasses

* fix(auth): harden user deletion and Telegram verification
This commit is contained in:
Seefs
2026-07-14 14:25:54 +08:00
committed by GitHub
parent 7c28993f6b
commit b6b97a66e3
8 changed files with 371 additions and 51 deletions
+55 -19
View File
@@ -54,12 +54,12 @@ func GetTwoFAByUserId(userId int) (*TwoFA, error) {
}
// IsTwoFAEnabled 检查用户是否启用了2FA
func IsTwoFAEnabled(userId int) bool {
func IsTwoFAEnabled(userId int) (bool, error) {
twoFA, err := GetTwoFAByUserId(userId)
if err != nil || twoFA == nil {
return false
if err != nil {
return false, err
}
return twoFA.IsEnabled
return twoFA != nil && twoFA.IsEnabled, nil
}
// CreateTwoFA 创建2FA设置
@@ -120,15 +120,50 @@ func (t *TwoFA) ResetFailedAttempts() error {
// IncrementFailedAttempts 增加失败尝试次数
func (t *TwoFA) IncrementFailedAttempts() error {
t.FailedAttempts++
// 检查是否需要锁定
if t.FailedAttempts >= common.MaxFailAttempts {
lockUntil := time.Now().Add(time.Duration(common.LockoutDuration) * time.Second)
t.LockedUntil = &lockUntil
if t.Id == 0 {
return errors.New("2FA记录ID不能为空")
}
return t.Update()
const maxUpdateRetries = 5
for range maxUpdateRetries {
var current TwoFA
if err := DB.Select("id", "failed_attempts", "locked_until").First(&current, t.Id).Error; err != nil {
return err
}
now := time.Now()
if current.LockedUntil != nil && now.Before(*current.LockedUntil) {
t.FailedAttempts = current.FailedAttempts
t.LockedUntil = current.LockedUntil
return nil
}
nextFailedAttempts := current.FailedAttempts + 1
nextLockedUntil := current.LockedUntil
if nextFailedAttempts >= common.MaxFailAttempts {
lockUntil := now.Add(time.Duration(common.LockoutDuration) * time.Second)
nextLockedUntil = &lockUntil
}
result := DB.Model(&TwoFA{}).
Where("id = ? AND failed_attempts = ? AND (locked_until IS NULL OR locked_until <= ?)", current.Id, current.FailedAttempts, now).
Updates(map[string]interface{}{
"failed_attempts": nextFailedAttempts,
"locked_until": nextLockedUntil,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
continue
}
t.FailedAttempts = nextFailedAttempts
t.LockedUntil = nextLockedUntil
return nil
}
return errors.New("更新2FA失败次数冲突,请重试")
}
// IsLocked 检查账户是否被锁定
@@ -186,16 +221,17 @@ func ValidateBackupCode(userId int, code string) (bool, error) {
// 验证备用码
for _, bc := range backupCodes {
if common.ValidatePasswordAndHash(normalizedCode, bc.CodeHash) {
// 标记为已使用
now := time.Now()
bc.IsUsed = true
bc.UsedAt = &now
if err := DB.Save(&bc).Error; err != nil {
return false, err
result := DB.Model(&TwoFABackupCode{}).
Where("id = ? AND is_used = ?", bc.Id, false).
Updates(map[string]interface{}{
"is_used": true,
"used_at": now,
})
if result.Error != nil {
return false, result.Error
}
return true, nil
return result.RowsAffected == 1, nil
}
}