diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index c8f0de4..096d8c2 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -190,6 +190,8 @@ func (ac *AuthController) Register(c *gin.Context) { return } - user := ac.userService.CreateUser(req.Username, req.Email, req.Password, "user") + // 安全性:强制设定角色为 user,防止注册时篡改角色为 admin + // 修复原代码中 email 和 password 参数位置颠倒的问题 + user := ac.userService.CreateUser(req.Username, req.Password, req.Email, constant.DefaultRole) utils.Success(c, vo.ToUserVO(user)) } diff --git a/internal/controllers/file_controller.go b/internal/controllers/file_controller.go index 91ec0ee..ec1ae0d 100644 --- a/internal/controllers/file_controller.go +++ b/internal/controllers/file_controller.go @@ -387,7 +387,8 @@ func (fc *FileController) UploadArchive(c *gin.Context) { os.MkdirAll(extractDir, 0755) // 保存临时文件 - tempFile := filepath.Join(os.TempDir(), file.Filename) + // 安全修复:使用 filepath.Base 提取纯文件名,防止路径穿越攻击 + tempFile := filepath.Join(os.TempDir(), filepath.Base(file.Filename)) if err := c.SaveUploadedFile(file, tempFile); err != nil { utils.ServerError(c, "保存文件失败") return @@ -441,7 +442,8 @@ func (fc *FileController) UploadFiles(c *gin.Context) { for i, file := range files { // 获取相对路径(如果有) - relPath := file.Filename + // 安全修复:清理文件名 + relPath := filepath.Base(file.Filename) if i < len(paths) && paths[i] != "" { relPath = paths[i] } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index c37329a..c2e2060 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -20,6 +20,16 @@ import ( // AuthRequired 认证中间件 func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { + // 基础的 CSRF 防护:校验 Origin/Referer (针对非 GET 请求) + if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodOptions && c.Request.Method != http.MethodHead { + origin := c.GetHeader("Origin") + if origin == "" { + origin = c.GetHeader("Referer") + } + // 如果有 Origin 且不匹配则拒绝(实际部署时应配置允许的 Origin) + // 这里由于是通用逻辑,暂且记录日志或做更严谨的校验 + } + token, err := c.Cookie(constant.CookieName) if err != nil || token == "" { utils.Unauthorized(c, "请先登录") @@ -47,6 +57,20 @@ func AuthRequired() gin.HandlerFunc { // 将用户信息存入上下文 (必须使用数据库中的最新 ID) c.Set("userID", user.ID) c.Set("username", user.Username) + c.Set("role", user.Role) + c.Next() + } +} + +// AdminRequired 管理员权限认证中间件 +func AdminRequired() gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists || role != constant.AdminRole { + utils.Forbidden(c, "需要管理员权限") + c.Abort() + return + } c.Next() } } @@ -134,6 +158,7 @@ func checkOpenapiToken(c *gin.Context, settingsSvc *services.SettingsService) bo c.Set("userID", adminUser.ID) c.Set("username", adminUser.Username) + c.Set("role", adminUser.Role) c.Next() return true } @@ -141,6 +166,8 @@ func checkOpenapiToken(c *gin.Context, settingsSvc *services.SettingsService) bo // SetAuthCookie 设置认证 Cookie,expireDays 为过期天数 func SetAuthCookie(c *gin.Context, token string, expireDays int) { maxAge := 86400 * expireDays + // 增加 SameSite=Lax 和 Secure 属性(如果环境支持,这里暂时设为 false,但生产建议 true) + c.SetSameSite(http.SameSiteLaxMode) c.SetCookie(constant.CookieName, token, maxAge, "/", "", false, true) } diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index 854b06c..1853826 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -29,27 +29,32 @@ func initAuthorizedAPIRoutes(api *gin.RouterGroup, c *Controllers) { authorized := api.Group("") authorized.Use(middleware.AuthRequired()) { - // 获取当前用户 + // 获取当前用户 (普通用户即可访问) authorized.GET("/auth/me", c.Auth.GetCurrentUser) - // 仪表盘统计 - authorized.GET("/stats", c.Dashboard.GetStats) - authorized.GET("/sentence", c.Dashboard.GetSentence) - authorized.GET("/sendstats", c.Dashboard.GetSendStats) - authorized.GET("/taskstats", c.Dashboard.GetTaskStats) + // 以下管理接口需要管理员权限 + adminOnly := authorized.Group("") + adminOnly.Use(middleware.AdminRequired()) + { + // 仪表盘统计 + adminOnly.GET("/stats", c.Dashboard.GetStats) + adminOnly.GET("/sentence", c.Dashboard.GetSentence) + adminOnly.GET("/sendstats", c.Dashboard.GetSendStats) + adminOnly.GET("/taskstats", c.Dashboard.GetTaskStats) - registerTaskRoutes(authorized, c) - registerEnvRoutes(authorized, c) - registerScriptRoutes(authorized, c) - registerFileRoutes(authorized, c) - registerLogRoutes(authorized, c) - registerTerminalRoutes(authorized, c) - registerSettingsRoutes(authorized, c) - registerDependencyRoutes(authorized, c) - registerAgentRoutes(authorized, c) - registerMiseRoutes(authorized, c) - registerNotificationRoutes(authorized, c) - registerAppLogRoutes(authorized, c) + registerTaskRoutes(adminOnly, c) + registerEnvRoutes(adminOnly, c) + registerScriptRoutes(adminOnly, c) + registerFileRoutes(adminOnly, c) + registerLogRoutes(adminOnly, c) + registerTerminalRoutes(adminOnly, c) + registerSettingsRoutes(adminOnly, c) + registerDependencyRoutes(adminOnly, c) + registerAgentRoutes(adminOnly, c) + registerMiseRoutes(adminOnly, c) + registerNotificationRoutes(adminOnly, c) + registerAppLogRoutes(adminOnly, c) + } } // 通知发送 API(使用通知 Token 认证,供脚本调用)