fix(security): security audit fixes for API vulnerabilities
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:
@@ -135,18 +135,7 @@ func (s *adminCommentServiceImpl) convertCommentToAdminListResponse(comment *mod
|
||||
// 作者信息
|
||||
var author *dto.UserResponse
|
||||
if comment.User != nil {
|
||||
author = &dto.UserResponse{
|
||||
ID: comment.User.ID,
|
||||
Username: comment.User.Username,
|
||||
Nickname: comment.User.Nickname,
|
||||
Email: comment.User.Email,
|
||||
Phone: comment.User.Phone,
|
||||
Avatar: comment.User.Avatar,
|
||||
Bio: comment.User.Bio,
|
||||
Website: comment.User.Website,
|
||||
Location: comment.User.Location,
|
||||
CreatedAt: comment.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
author = dto.ConvertUserToResponse(comment.User)
|
||||
}
|
||||
|
||||
// 帖子简要信息
|
||||
@@ -197,18 +186,7 @@ func (s *adminCommentServiceImpl) convertCommentToAdminDetailResponse(comment *m
|
||||
// 作者信息
|
||||
var author *dto.UserResponse
|
||||
if comment.User != nil {
|
||||
author = &dto.UserResponse{
|
||||
ID: comment.User.ID,
|
||||
Username: comment.User.Username,
|
||||
Nickname: comment.User.Nickname,
|
||||
Email: comment.User.Email,
|
||||
Phone: comment.User.Phone,
|
||||
Avatar: comment.User.Avatar,
|
||||
Bio: comment.User.Bio,
|
||||
Website: comment.User.Website,
|
||||
Location: comment.User.Location,
|
||||
CreatedAt: comment.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
author = dto.ConvertUserToResponse(comment.User)
|
||||
}
|
||||
|
||||
// 帖子简要信息
|
||||
|
||||
@@ -119,18 +119,7 @@ func (s *adminGroupServiceImpl) GetGroupDetail(ctx context.Context, groupID stri
|
||||
if group.OwnerID != "" {
|
||||
owner, err := s.userRepo.GetByID(group.OwnerID)
|
||||
if err == nil && owner != nil {
|
||||
response.Owner = &dto.UserResponse{
|
||||
ID: owner.ID,
|
||||
Username: owner.Username,
|
||||
Nickname: owner.Nickname,
|
||||
Email: owner.Email,
|
||||
Phone: owner.Phone,
|
||||
Avatar: owner.Avatar,
|
||||
Bio: owner.Bio,
|
||||
Website: owner.Website,
|
||||
Location: owner.Location,
|
||||
CreatedAt: dto.FormatTime(owner.CreatedAt),
|
||||
}
|
||||
response.Owner = dto.ConvertUserToResponse(owner)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,18 +274,7 @@ func convertPostToAdminListResponse(post *model.Post) dto.AdminPostListResponse
|
||||
|
||||
var author *dto.UserResponse
|
||||
if post.User != nil {
|
||||
author = &dto.UserResponse{
|
||||
ID: post.User.ID,
|
||||
Username: post.User.Username,
|
||||
Nickname: post.User.Nickname,
|
||||
Email: post.User.Email,
|
||||
Phone: post.User.Phone,
|
||||
Avatar: post.User.Avatar,
|
||||
Bio: post.User.Bio,
|
||||
Website: post.User.Website,
|
||||
Location: post.User.Location,
|
||||
CreatedAt: post.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
author = dto.ConvertUserToResponse(post.User)
|
||||
}
|
||||
|
||||
return dto.AdminPostListResponse{
|
||||
@@ -326,18 +315,7 @@ func convertPostToAdminDetailResponse(post *model.Post) *dto.AdminPostDetailResp
|
||||
|
||||
var author *dto.UserResponse
|
||||
if post.User != nil {
|
||||
author = &dto.UserResponse{
|
||||
ID: post.User.ID,
|
||||
Username: post.User.Username,
|
||||
Nickname: post.User.Nickname,
|
||||
Email: post.User.Email,
|
||||
Phone: post.User.Phone,
|
||||
Avatar: post.User.Avatar,
|
||||
Bio: post.User.Bio,
|
||||
Website: post.User.Website,
|
||||
Location: post.User.Location,
|
||||
CreatedAt: post.User.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
author = dto.ConvertUserToResponse(post.User)
|
||||
}
|
||||
|
||||
return &dto.AdminPostDetailResponse{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,13 +14,10 @@ import (
|
||||
|
||||
// UserService 用户服务接口
|
||||
type UserService interface {
|
||||
// 验证码相关
|
||||
SendRegisterCode(ctx context.Context, email string) error
|
||||
SendPasswordResetCode(ctx context.Context, email string) error
|
||||
SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error
|
||||
SendChangePasswordCode(ctx context.Context, userID string) error
|
||||
|
||||
// 邮箱验证
|
||||
SendRegisterCode(ctx context.Context, email, clientIP string) error
|
||||
SendPasswordResetCode(ctx context.Context, email, clientIP string) error
|
||||
SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email, clientIP string) error
|
||||
SendChangePasswordCode(ctx context.Context, userID, clientIP string) error
|
||||
VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error
|
||||
|
||||
// 用户认证
|
||||
@@ -89,25 +86,23 @@ func (s *userServiceImpl) SetLogService(logService *LogService) {
|
||||
}
|
||||
|
||||
// SendRegisterCode 发送注册验证码
|
||||
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email string) error {
|
||||
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email, clientIP string) error {
|
||||
user, err := s.userRepo.GetByEmail(email)
|
||||
if err == nil && user != nil {
|
||||
return ErrEmailExists
|
||||
}
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposeRegister, email)
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposeRegister, email, clientIP)
|
||||
}
|
||||
|
||||
// SendPasswordResetCode 发送找回密码验证码
|
||||
func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email string) error {
|
||||
func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email, clientIP string) error {
|
||||
user, err := s.userRepo.GetByEmail(email)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposePasswordReset, email)
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposePasswordReset, email, clientIP)
|
||||
}
|
||||
|
||||
// SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码
|
||||
func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error {
|
||||
func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email, clientIP string) error {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
@@ -130,7 +125,7 @@ func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, us
|
||||
return ErrEmailExists
|
||||
}
|
||||
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposeEmailVerify, targetEmail)
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposeEmailVerify, targetEmail, clientIP)
|
||||
}
|
||||
|
||||
// VerifyCurrentUserEmail 验证当前用户邮箱
|
||||
@@ -163,7 +158,7 @@ func (s *userServiceImpl) VerifyCurrentUserEmail(ctx context.Context, userID, em
|
||||
}
|
||||
|
||||
// SendChangePasswordCode 发送修改密码验证码
|
||||
func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID string) error {
|
||||
func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID, clientIP string) error {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
@@ -171,7 +166,7 @@ func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID str
|
||||
if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
|
||||
return ErrEmailNotBound
|
||||
}
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposeChangePassword, *user.Email)
|
||||
return s.emailCodeService.SendCode(ctx, CodePurposeChangePassword, *user.Email, clientIP)
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
|
||||
Reference in New Issue
Block a user