From 129c6c7a96f0f047890bd08a26a073e2f7a7e417 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 9 Jul 2026 15:51:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AF=86=E7=A0=81=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E4=B8=8A=E9=99=90=E6=A0=A1=E9=AA=8C=20+=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E4=BD=93=E5=A4=A7=E5=B0=8F=E9=99=90=E5=88=B6=E9=98=B2=20DoS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 validatePassword 校验函数(6-72 字节,bcrypt 硬上限),统一应用于 ChangePassword/CreateUser/UpdateUser,超长密码返回清晰提示而非迷惑性的"密码加密失败" - 新增 BodyLimit 中间件(http.MaxBytesReader),对所有 /api JSON 端点限制 1 MiB 请求体,防止内存耗尽型 DoS;/ws 走连接劫持不受影响 --- internal/handler/auth.go | 21 +++++++++++++++++++-- internal/handler/user.go | 9 +++++++++ internal/middleware/ratelimit.go | 9 +++++++++ internal/router/router.go | 8 +++++--- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/internal/handler/auth.go b/internal/handler/auth.go index e75bca4..12e3bfa 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -1,6 +1,7 @@ package handler import ( + "errors" "net/http" "time" @@ -13,6 +14,22 @@ import ( "golang.org/x/crypto/bcrypt" ) +const ( + minPasswordLen = 6 + maxPasswordLen = 72 // bcrypt 硬上限:超过 72 字节会被 bcrypt 截断或报错 +) + +func validatePassword(pw string) error { + n := len(pw) + if n < minPasswordLen { + return errors.New("密码长度不能少于6位") + } + if n > maxPasswordLen { + return errors.New("密码长度不能超过72字节") + } + return nil +} + type loginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` @@ -93,8 +110,8 @@ func ChangePassword(c *gin.Context) { return } - if len(req.NewPassword) < 6 { - c.JSON(http.StatusBadRequest, gin.H{"error": "新密码长度不能少于6位"}) + if err := validatePassword(req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } diff --git a/internal/handler/user.go b/internal/handler/user.go index 8ab78ea..ff5b3f3 100644 --- a/internal/handler/user.go +++ b/internal/handler/user.go @@ -82,6 +82,11 @@ func CreateUser(c *gin.Context) { return } + if err := validatePassword(req.Password); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) @@ -162,6 +167,10 @@ func UpdateUser(c *gin.Context) { } if req.Password != "" { + if err := validatePassword(req.Password); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go index 0d7e784..9fd3c76 100644 --- a/internal/middleware/ratelimit.go +++ b/internal/middleware/ratelimit.go @@ -84,3 +84,12 @@ func LoginRateLimit() gin.HandlerFunc { c.Next() } } + +// BodyLimit 限制请求体大小,防止内存耗尽型 DoS。 +// 对所有 /api JSON 端点生效;WebSocket 走连接劫持,握手阶段不受影响。 +func BodyLimit(max int64) gin.HandlerFunc { + return func(c *gin.Context) { + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, max) + c.Next() + } +} diff --git a/internal/router/router.go b/internal/router/router.go index e583548..34188ff 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -14,11 +14,13 @@ import ( func Setup(r *gin.Engine) { r.GET("/ws", vpn.HandleWS) - r.POST("/api/login", middleware.LoginRateLimit(), handler.Login) + bodyLimit := middleware.BodyLimit(1 << 20) // 1 MiB + + r.POST("/api/login", bodyLimit, middleware.LoginRateLimit(), handler.Login) r.GET("/api/version", handler.GetVersion) auth := r.Group("/api") - auth.Use(middleware.AuthMiddleware()) + auth.Use(middleware.AuthMiddleware(), bodyLimit) { auth.GET("/me", handler.Me) auth.PUT("/me/password", handler.ChangePassword) @@ -27,7 +29,7 @@ func Setup(r *gin.Engine) { } admin := r.Group("/api/admin") - admin.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware()) + admin.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware(), bodyLimit) { admin.GET("/stats", handler.GetAdminStats) admin.GET("/users/count", handler.GetUserCount)