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

@@ -15,6 +15,21 @@ import (
"carrot_bbs/internal/service"
)
const (
maxPageSize = 100
defaultPageSize = 20
)
func normalizePagination(page, pageSize int) (int, int) {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > maxPageSize {
pageSize = defaultPageSize
}
return page, pageSize
}
// UserHandler 用户处理器
type UserHandler struct {
userService service.UserService
@@ -110,7 +125,7 @@ func (h *UserHandler) Register(c *gin.Context) {
}
response.Success(c, gin.H{
"user": dto.ConvertUserToResponse(user),
"user": dto.ConvertUserToSelfResponse(user, 0),
"token": accessToken,
"refresh_token": refreshToken,
})
@@ -177,7 +192,7 @@ func (h *UserHandler) Login(c *gin.Context) {
}
response.Success(c, gin.H{
"user": dto.ConvertUserToResponse(user),
"user": dto.ConvertUserToSelfResponse(user, int(user.PostsCount)),
"token": accessToken,
"refresh_token": refreshToken,
})
@@ -195,7 +210,8 @@ func (h *UserHandler) SendRegisterCode(c *gin.Context) {
return
}
if err := h.userService.SendRegisterCode(c.Request.Context(), req.Email); err != nil {
clientIP := c.ClientIP()
if err := h.userService.SendRegisterCode(c.Request.Context(), req.Email, clientIP); err != nil {
response.HandleError(c, err, "failed to send register verification code")
return
}
@@ -203,7 +219,6 @@ func (h *UserHandler) SendRegisterCode(c *gin.Context) {
response.Success(c, gin.H{"success": true})
}
// SendPasswordResetCode 发送找回密码验证码
func (h *UserHandler) SendPasswordResetCode(c *gin.Context) {
type SendCodeRequest struct {
Email string `json:"email" binding:"required,email"`
@@ -215,7 +230,8 @@ func (h *UserHandler) SendPasswordResetCode(c *gin.Context) {
return
}
if err := h.userService.SendPasswordResetCode(c.Request.Context(), req.Email); err != nil {
clientIP := c.ClientIP()
if err := h.userService.SendPasswordResetCode(c.Request.Context(), req.Email, clientIP); err != nil {
response.HandleError(c, err, "failed to send reset verification code")
return
}
@@ -259,14 +275,12 @@ func (h *UserHandler) GetCurrentUser(c *gin.Context) {
return
}
// 实时计算帖子数量
postsCount, err := h.userService.GetUserPostCount(c.Request.Context(), userID)
if err != nil {
// 如果获取失败,使用数据库中的值
postsCount = int64(user.PostsCount)
}
response.Success(c, dto.ConvertUserToDetailResponseWithPostsCount(user, int(postsCount)))
response.Success(c, dto.ConvertUserToSelfResponse(user, int(postsCount)))
}
// GetUserByID 获取指定用户
@@ -382,7 +396,7 @@ func (h *UserHandler) SendEmailVerifyCode(c *gin.Context) {
return
}
if err := h.userService.SendCurrentUserEmailVerifyCode(c.Request.Context(), userID, req.Email); err != nil {
if err := h.userService.SendCurrentUserEmailVerifyCode(c.Request.Context(), userID, req.Email, c.ClientIP()); err != nil {
response.HandleError(c, err, "failed to send email verify code")
return
}
@@ -562,12 +576,7 @@ func (h *UserHandler) GetBlockedUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
page, pageSize = normalizePagination(page, pageSize)
users, total, err := h.userService.GetBlockedUsers(c.Request.Context(), currentUserID, page, pageSize)
if err != nil {
@@ -734,7 +743,7 @@ func (h *UserHandler) SendChangePasswordCode(c *gin.Context) {
return
}
err := h.userService.SendChangePasswordCode(c.Request.Context(), currentUserID)
err := h.userService.SendChangePasswordCode(c.Request.Context(), currentUserID, c.ClientIP())
if err != nil {
response.HandleError(c, err, "failed to send change password code")
return
@@ -782,6 +791,7 @@ func (h *UserHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
users, total, err := h.userService.Search(c.Request.Context(), keyword, page, pageSize)
if err != nil {