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

@@ -7,8 +7,27 @@ import (
// ==================== User DTOs ====================
// UserResponse 用户信息响应
// UserResponse 用户公开信息响应(不包含敏感信息)
type UserResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
EmailVerified bool `json:"email_verified"`
Avatar string `json:"avatar"`
CoverURL string `json:"cover_url"`
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
PostsCount int `json:"posts_count"`
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
IsFollowing bool `json:"is_following"`
IsFollowingMe bool `json:"is_following_me"`
CreatedAt string `json:"created_at"`
}
// UserSelfResponse 用户自身信息响应(包含敏感信息,仅本人可见)
type UserSelfResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
@@ -16,28 +35,7 @@ type UserResponse struct {
Phone *string `json:"phone,omitempty"`
EmailVerified bool `json:"email_verified"`
Avatar string `json:"avatar"`
CoverURL string `json:"cover_url"` // 头图URL
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
PostsCount int `json:"posts_count"`
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
IsFollowing bool `json:"is_following"` // 当前用户是否关注了该用户
IsFollowingMe bool `json:"is_following_me"` // 该用户是否关注了当前用户
CreatedAt string `json:"created_at"`
}
// UserDetailResponse 用户详情响应
type UserDetailResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email *string `json:"email"`
EmailVerified bool `json:"email_verified"`
Phone *string `json:"phone,omitempty"` // 仅当前用户自己可见
Avatar string `json:"avatar"`
CoverURL string `json:"cover_url"` // 头图URL
CoverURL string `json:"cover_url"`
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
@@ -45,8 +43,28 @@ type UserDetailResponse struct {
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
IsVerified bool `json:"is_verified"`
IsFollowing bool `json:"is_following"` // 当前用户是否关注了该用户
IsFollowingMe bool `json:"is_following_me"` // 该用户是否关注了当前用户
CreatedAt string `json:"created_at"`
}
// UserDetailResponse 用户详情响应(仅本人可见,包含敏感信息)
type UserDetailResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email *string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified"`
Phone *string `json:"phone,omitempty"`
Avatar string `json:"avatar"`
CoverURL string `json:"cover_url"`
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
PostsCount int `json:"posts_count"`
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
IsVerified bool `json:"is_verified"`
IsFollowing bool `json:"is_following"`
IsFollowingMe bool `json:"is_following_me"`
CreatedAt string `json:"created_at"`
}

View File

@@ -12,7 +12,7 @@ func getAvatarOrDefault(user *model.User) string {
return utils.GetAvatarOrDefault(user.Username, user.Nickname, user.Avatar)
}
// ConvertUserToResponse 将User转换为UserResponse
// ConvertUserToResponse 将User转换为UserResponse(公开信息,不包含敏感数据)
func ConvertUserToResponse(user *model.User) *UserResponse {
if user == nil {
return nil
@@ -21,8 +21,6 @@ func ConvertUserToResponse(user *model.User) *UserResponse {
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
@@ -36,7 +34,32 @@ func ConvertUserToResponse(user *model.User) *UserResponse {
}
}
// ConvertUserToResponseWithFollowing 将User转换为UserResponse包含关注状态
// ConvertUserToSelfResponse 将User转换为UserSelfResponse本人信息,包含敏感数据
func ConvertUserToSelfResponse(user *model.User, postsCount int) *UserSelfResponse {
if user == nil {
return nil
}
return &UserSelfResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToResponseWithFollowing 将User转换为UserResponse包含关注状态公开信息
func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *UserResponse {
if user == nil {
return nil
@@ -45,8 +68,6 @@ func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *Use
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
@@ -57,12 +78,12 @@ func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *Use
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsFollowing: isFollowing,
IsFollowingMe: false, // 默认false需要单独计算
IsFollowingMe: false,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToResponseWithPostsCount 将User转换为UserResponse使用实时计算的帖子数量
// ConvertUserToResponseWithPostsCount 将User转换为UserResponse使用实时计算的帖子数量,公开信息
func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *UserResponse {
if user == nil {
return nil
@@ -71,8 +92,6 @@ func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *User
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
@@ -86,7 +105,7 @@ func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *User
}
}
// ConvertUserToResponseWithMutualFollow 将User转换为UserResponse包含双向关注状态
// ConvertUserToResponseWithMutualFollow 将User转换为UserResponse包含双向关注状态,公开信息
func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFollowingMe bool) *UserResponse {
if user == nil {
return nil
@@ -95,8 +114,6 @@ func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFoll
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
@@ -112,7 +129,7 @@ func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFoll
}
}
// ConvertUserToResponseWithMutualFollowAndPostsCount 将User转换为UserResponse包含双向关注状态和实时计算的帖子数量
// ConvertUserToResponseWithMutualFollowAndPostsCount 将User转换为UserResponse包含双向关注状态和实时计算的帖子数量,公开信息
func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFollowing, isFollowingMe bool, postsCount int) *UserResponse {
if user == nil {
return nil
@@ -121,8 +138,6 @@ func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFoll
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
@@ -138,7 +153,7 @@ func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFoll
}
}
// ConvertUserToDetailResponse 将User转换为UserDetailResponse
// ConvertUserToDetailResponse 将User转换为UserDetailResponse(仅本人可见,包含敏感信息)
func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
if user == nil {
return nil
@@ -162,7 +177,7 @@ func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
}
}
// ConvertUserToDetailResponseWithPostsCount 将User转换为UserDetailResponse使用实时计算的帖子数量
// ConvertUserToDetailResponseWithPostsCount 将User转换为UserDetailResponse使用实时计算的帖子数量,仅本人可见
func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int) *UserDetailResponse {
if user == nil {
return nil
@@ -173,7 +188,7 @@ func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int)
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Phone: user.Phone, // 仅当前用户自己可见
Phone: user.Phone,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,