fix(security): security audit fixes for API vulnerabilities
All checks were successful
Build Backend / build (push) Successful in 4m47s
Build Backend / build-docker (push) Successful in 2m22s

Security fixes:
- Fix sensitive data exposure: UserResponse no longer contains email/phone
- Add UserSelfResponse for authenticated user's own data
- Protect Gorse endpoints with admin role requirement
- Add rate limiting (10 req/min) to auth endpoints
- Fix CORS configuration to use allowed origins list
- Add pagination parameter validation (max 100 per page)

Changes:
- dto/dto.go: Add UserSelfResponse type
- dto/user_converter.go: Add ConvertUserToSelfResponse
- handler/user_handler.go: Use secure response types
- middleware/cors.go: Implement origin whitelist
- middleware/ratelimit.go: Enhance rate limiter
- router/router.go: Add auth rate limit, protect Gorse
- service/admin_*.go: Use ConvertUserToResponse
This commit is contained in:
lafay
2026-03-19 13:49:51 +08:00
parent d387cc493e
commit b0f209fdf8
13 changed files with 221 additions and 162 deletions

View File

@@ -14,8 +14,10 @@ import (
)
const (
verifyCodeTTL = 10 * time.Minute
verifyCodeRateLimitTTL = 60 * time.Second
verifyCodeTTL = 10 * time.Minute
verifyCodeRateLimitTTL = 60 * time.Second
verifyCodeIPRateLimitTTL = 60 * time.Second
verifyCodeIPRateLimitCount = 5
)
const (
@@ -33,7 +35,7 @@ type verificationCodePayload struct {
}
type EmailCodeService interface {
SendCode(ctx context.Context, purpose, email string) error
SendCode(ctx context.Context, purpose, email, clientIP string) error
VerifyCode(purpose, email, code string) error
}
@@ -57,6 +59,10 @@ func verificationCodeRateLimitKey(purpose, email string) string {
return fmt.Sprintf("auth:verify_code_rate_limit:%s:%s", purpose, strings.ToLower(strings.TrimSpace(email)))
}
func verificationCodeIPRateLimitKey(clientIP string) string {
return fmt.Sprintf("auth:verify_code_ip_rate_limit:%s", clientIP)
}
func generateNumericCode(length int) (string, error) {
if length <= 0 {
return "", fmt.Errorf("invalid code length")
@@ -73,7 +79,7 @@ func generateNumericCode(length int) (string, error) {
return string(result), nil
}
func (s *emailCodeServiceImpl) SendCode(ctx context.Context, purpose, email string) error {
func (s *emailCodeServiceImpl) SendCode(ctx context.Context, purpose, email, clientIP string) error {
if strings.TrimSpace(email) == "" || !utils.ValidateEmail(email) {
return ErrInvalidEmail
}
@@ -89,6 +95,15 @@ func (s *emailCodeServiceImpl) SendCode(ctx context.Context, purpose, email stri
return ErrVerificationCodeTooFrequent
}
if clientIP != "" {
ipRateLimitKey := verificationCodeIPRateLimitKey(clientIP)
if count, ok := s.cache.Get(ipRateLimitKey); ok {
if c, ok := count.(int); ok && c >= verifyCodeIPRateLimitCount {
return ErrVerificationCodeTooFrequent
}
}
}
code, err := generateNumericCode(6)
if err != nil {
return fmt.Errorf("generate verification code failed: %w", err)
@@ -103,6 +118,19 @@ func (s *emailCodeServiceImpl) SendCode(ctx context.Context, purpose, email stri
s.cache.Set(cacheKey, payload, verifyCodeTTL)
s.cache.Set(rateLimitKey, "1", verifyCodeRateLimitTTL)
if clientIP != "" {
ipRateLimitKey := verificationCodeIPRateLimitKey(clientIP)
if count, ok := s.cache.Get(ipRateLimitKey); ok {
if c, ok := count.(int); ok {
s.cache.Set(ipRateLimitKey, c+1, verifyCodeIPRateLimitTTL)
} else {
s.cache.Set(ipRateLimitKey, 1, verifyCodeIPRateLimitTTL)
}
} else {
s.cache.Set(ipRateLimitKey, 1, verifyCodeIPRateLimitTTL)
}
}
subject, sceneText := verificationEmailMeta(purpose)
textBody := fmt.Sprintf("【%s】验证码%s\n有效期10分钟\n请勿将验证码泄露给他人。", sceneText, code)
htmlBody := buildVerificationEmailHTML(sceneText, code)