fix: 密码长度上限校验 + 请求体大小限制防 DoS
- 新增 validatePassword 校验函数(6-72 字节,bcrypt 硬上限),统一应用于 ChangePassword/CreateUser/UpdateUser,超长密码返回清晰提示而非迷惑性的"密码加密失败" - 新增 BodyLimit 中间件(http.MaxBytesReader),对所有 /api JSON 端点限制 1 MiB 请求体,防止内存耗尽型 DoS;/ws 走连接劫持不受影响
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +14,22 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"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 {
|
type loginRequest struct {
|
||||||
Username string `json:"username" binding:"required"`
|
Username string `json:"username" binding:"required"`
|
||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password" binding:"required"`
|
||||||
@@ -93,8 +110,8 @@ func ChangePassword(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.NewPassword) < 6 {
|
if err := validatePassword(req.NewPassword); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "新密码长度不能少于6位"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ func CreateUser(c *gin.Context) {
|
|||||||
return
|
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)
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||||
@@ -162,6 +167,10 @@ func UpdateUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Password != "" {
|
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)
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||||
|
|||||||
@@ -84,3 +84,12 @@ func LoginRateLimit() gin.HandlerFunc {
|
|||||||
c.Next()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import (
|
|||||||
func Setup(r *gin.Engine) {
|
func Setup(r *gin.Engine) {
|
||||||
r.GET("/ws", vpn.HandleWS)
|
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)
|
r.GET("/api/version", handler.GetVersion)
|
||||||
|
|
||||||
auth := r.Group("/api")
|
auth := r.Group("/api")
|
||||||
auth.Use(middleware.AuthMiddleware())
|
auth.Use(middleware.AuthMiddleware(), bodyLimit)
|
||||||
{
|
{
|
||||||
auth.GET("/me", handler.Me)
|
auth.GET("/me", handler.Me)
|
||||||
auth.PUT("/me/password", handler.ChangePassword)
|
auth.PUT("/me/password", handler.ChangePassword)
|
||||||
@@ -27,7 +29,7 @@ func Setup(r *gin.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
admin := r.Group("/api/admin")
|
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("/stats", handler.GetAdminStats)
|
||||||
admin.GET("/users/count", handler.GetUserCount)
|
admin.GET("/users/count", handler.GetUserCount)
|
||||||
|
|||||||
Reference in New Issue
Block a user