fix confirmed deployment risks
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled

This commit is contained in:
2026-04-25 09:24:17 +08:00
parent 75d03e4713
commit 649eb23091
10 changed files with 258 additions and 19 deletions

View File

@@ -1,12 +1,23 @@
package admin
import (
"sync"
"time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
const (
restorePasswordMaxFailures = 5
restorePasswordBlockWindow = 15 * time.Minute
)
var backupRestoreAttemptLimiter = newRestoreAttemptLimiter(restorePasswordMaxFailures, restorePasswordBlockWindow)
type BackupHandler struct {
backupService *service.BackupService
userService *service.UserService
@@ -165,6 +176,64 @@ type RestoreBackupRequest struct {
Password string `json:"password" binding:"required"`
}
type restoreAttemptState struct {
failuresUntil time.Time
failureCount int
}
type restoreAttemptLimiter struct {
mu sync.Mutex
maxFailures int
window time.Duration
states map[int64]restoreAttemptState
}
func newRestoreAttemptLimiter(maxFailures int, window time.Duration) *restoreAttemptLimiter {
return &restoreAttemptLimiter{
maxFailures: maxFailures,
window: window,
states: make(map[int64]restoreAttemptState),
}
}
func (l *restoreAttemptLimiter) allow(userID int64, now time.Time) (bool, time.Duration) {
l.mu.Lock()
defer l.mu.Unlock()
state, ok := l.states[userID]
if !ok {
return true, 0
}
if now.After(state.failuresUntil) {
delete(l.states, userID)
return true, 0
}
if state.failureCount < l.maxFailures {
return true, 0
}
return false, state.failuresUntil.Sub(now)
}
func (l *restoreAttemptLimiter) recordFailure(userID int64, now time.Time) {
l.mu.Lock()
defer l.mu.Unlock()
state := l.states[userID]
if now.After(state.failuresUntil) {
state = restoreAttemptState{}
}
state.failureCount++
state.failuresUntil = now.Add(l.window)
l.states[userID] = state
}
func (l *restoreAttemptLimiter) recordSuccess(userID int64) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.states, userID)
}
func (h *BackupHandler) RestoreBackup(c *gin.Context) {
backupID := c.Param("id")
if backupID == "" {
@@ -185,6 +254,11 @@ func (h *BackupHandler) RestoreBackup(c *gin.Context) {
return
}
if allowed, _ := backupRestoreAttemptLimiter.allow(sub.UserID, time.Now()); !allowed {
response.ErrorFrom(c, infraerrors.TooManyRequests("RESTORE_PASSWORD_RATE_LIMITED", "too many failed password attempts, please try again later"))
return
}
// 获取管理员用户并验证密码
user, err := h.userService.GetByID(c.Request.Context(), sub.UserID)
if err != nil {
@@ -192,9 +266,11 @@ func (h *BackupHandler) RestoreBackup(c *gin.Context) {
return
}
if !user.CheckPassword(req.Password) {
backupRestoreAttemptLimiter.recordFailure(sub.UserID, time.Now())
response.BadRequest(c, "incorrect admin password")
return
}
backupRestoreAttemptLimiter.recordSuccess(sub.UserID)
record, err := h.backupService.StartRestore(c.Request.Context(), backupID)
if err != nil {