# 权限系统深度审计报告 > 审计时间:2026-04-17 > 审计范围:`perms-system-server` 全部业务源码(测试代码除外) > 审计维度:逻辑一致性、并发与竞态、资源管理、数据完整性、安全漏洞、边界崩溃 --- ## 🚩 核心逻辑漏洞 (High Risk) ### 1. UserList 接口数据越权泄漏 - **文件**:`internal/logic/user/userListLogic.go` 第 45 行 - **描述**:非超管用户(产品管理员)调用 UserList 时,虽然校验了 `caller.ProductCode == req.ProductCode`,但底层查询 `SysUserModel.FindListByPage` 执行的是 **全表分页查询**(无任何 WHERE 条件),返回了系统中**所有用户**,而非仅当前产品的成员用户。memberMap 只是在返回结果上附加了 memberType 信息,并不过滤非成员用户。 - **影响**:产品管理员可以看到其他产品甚至全系统的用户信息(用户名、昵称、邮箱、手机号、部门等),构成**水平越权数据泄漏**。 - **修复方案**:非超管用户查询时,应先查 `sys_product_member` 获取当前产品的成员 userId 列表,再用这些 userId 进行分页查询。示例: ```go // userListLogic.go - 非超管场景 if req.ProductCode != "" && !caller.IsSuperAdmin { list, total, err := l.svcCtx.SysUserModel.FindListByProductMembers( l.ctx, req.ProductCode, page, pageSize, ) // ... } ``` 需要在 Model 层新增 `FindListByProductMembers` 方法,JOIN `sys_product_member` 表过滤。 --- ### 2. BindRoles 缺少角色级别校验,存在提权漏洞 - **文件**:`internal/logic/user/bindRolesLogic.go` 第 33-101 行 - **描述**:`BindRoles` 仅通过 `CheckManageAccess` 验证操作者对**目标用户当前状态**的管理权限,但未校验所绑定角色的 `permsLevel` 是否在操作者自身权限范围内。攻击路径: 1. 操作者 A(permsLevel=50)管理目标用户 B(当前 permsLevel=100) 2. `CheckManageAccess` 通过(50 < 100,A 的权限高于 B) 3. A 给 B 绑定一个 permsLevel=1 的角色 4. B 的 permsLevel 变为 1,权限反超操作者 A - **影响**:任何拥有用户管理权限的操作者可以通过分配高等级角色来实现**权限提升**,使目标用户获得超过自身的权限级别。 - **修复方案**:在校验角色有效性的循环中,增加 permsLevel 校验: ```go // bindRolesLogic.go - 在遍历 roles 时增加 caller := middleware.GetUserDetails(l.ctx) for _, r := range roles { if r.ProductCode != productCode { return response.ErrBadRequest("不能绑定其他产品的角色") } if r.Status != consts.StatusEnabled { return response.ErrBadRequest("不能绑定已禁用的角色") } // 非超管不能分配超出自身权限级别的角色 if !caller.IsSuperAdmin && r.PermsLevel < caller.MinPermsLevel { return response.ErrForbidden("不能分配权限级别高于自身的角色") } } ``` --- ### 3. appSecret 数据库明文存储 - **文件**:`perm.sql` 第 15 行;`internal/logic/pub/syncPermsLogic.go` 第 37 行;`internal/server/permserver.go` 第 40 行 - **描述**:`sys_product.appSecret` 以明文存储在数据库中。当前仅用 `subtle.ConstantTimeCompare` 进行比对(防时序攻击)。若数据库被拖库或备份泄漏,所有产品的 appSecret 直接暴露。 - **影响**:数据库泄漏场景下,攻击者可以使用任何产品的 appSecret 调用 `SyncPerms` 接口,篡改该产品的全部权限定义,影响所有用户的权限体系。 - **修复方案**:将 appSecret 使用 bcrypt 或 HMAC-SHA256 哈希后存储。`CreateProduct` 时仅一次性返回原文,之后只存哈希值。验证时改用 `bcrypt.CompareHashAndPassword`: ```go // syncPermsLogic.go / permserver.go if err := bcrypt.CompareHashAndPassword( []byte(product.AppSecretHash), []byte(req.AppSecret), ); err != nil { return nil, response.ErrUnauthorized("appSecret验证失败") } ``` --- ### 4. UpdateUser 权限模型与其他接口不一致,产品管理员无法修改自己创建的用户 - **文件**:`internal/logic/user/updateUserLogic.go` 第 37-45 行 - **描述**:`UpdateUser` 的权限判断逻辑为"只有超管或用户自身可修改",而系统中 `UpdateUserStatus`、`BindRoles`、`SetUserPerms` 等接口均使用 `CheckManageAccess`(支持产品管理员和部门层级管理)。这导致产品管理员可以创建用户(`CreateUser`)、冻结用户(`UpdateUserStatus`)、绑定角色(`BindRoles`),却**无法修改**用户的昵称、邮箱、部门等基本信息。 - **影响**:产品管理员的管理权限出现逻辑断裂——能创建和冻结用户,但不能编辑用户信息,不得不依赖超管操作,严重影响管理效率。 - **修复方案**:将 UpdateUser 的权限模型统一为 `CheckManageAccess`,并保留对自身修改的限制(不能改自己的部门和状态): ```go func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserReq) error { caller := middleware.GetUserDetails(l.ctx) if caller == nil { return response.ErrUnauthorized("未登录") } if caller.UserId == req.Id { if req.DeptId != nil || req.Status != 0 { return response.ErrForbidden("不允许修改自己的部门和状态") } } else { productCode := middleware.GetProductCode(l.ctx) if err := authHelper.CheckManageAccess(l.ctx, l.svcCtx, req.Id, productCode); err != nil { return err } } // ... 后续逻辑不变 } ``` --- ## ⚠️ 健壮性与性能建议 (Medium/Low) ### 5. [Medium] CreateUser 未将用户加入产品成员,工作流存在断裂 - **文件**:`internal/logic/user/createUserLogic.go` - **描述**:`CreateUser` 要求 `RequireProductAdminFor(productCode)` 校验产品管理员身份,但创建用户后并未将其加入当前产品的成员列表。新建用户需要管理员再单独调用 `AddMember` 才能登录和使用产品。 - **影响**:管理员可能误以为创建用户即完成了入组操作,导致新用户无法登录。增加了操作步骤和出错概率。 - **建议**:考虑在 CreateUser 中增加可选参数 `memberType`,当传入时在同一事务中自动创建产品成员记录;或在文档/前端层面明确引导管理员完成两步操作。 --- ### 6. [Medium] 缓存前缀变量是包级可变全局状态 - **文件**:各 `*Model_gen.go`(如 `sysUserModel_gen.go` 第 26-27 行,`sysPermModel_gen.go` 第 26-27 行) - **描述**:`cacheSysUserIdPrefix`、`cacheSysPermIdPrefix` 等缓存前缀在 `newSys*Model` 函数中被直接修改(`cacheSysUserIdPrefix = cachePrefix + ":cache:sysUser:id:"`)。这些是 `var` 级别的全局变量。 - **影响**:若代码中有多处以不同 `cachePrefix` 创建同类 Model 实例(当前不存在此情况),会产生竞态条件。虽然当前启动时仅初始化一次,但这种模式在代码演进中存在隐患。 - **建议**:由于此代码由 goctl 生成,短期内可接受。长期建议修改 goctl 模板,将前缀存入 struct 字段而非修改包级变量。 --- ### 7. [Medium] AddMember 并发重复请求错误信息不友好 - **文件**:`internal/logic/member/addMemberLogic.go` 第 52-55 行 - **描述**:使用 check-then-insert 模式检查成员是否已存在。若两个请求并发执行,都通过了检查,其中一个在 Insert 时会触发数据库唯一约束错误(`uk_product_user`),但此错误未被捕获转换为友好提示,而是返回原始的 500 错误。 - **影响**:并发场景下用户收到 "服务器内部错误" 而非 "该用户已是该产品成员"。 - **建议**:在 Insert 错误处理中捕获唯一约束冲突: ```go result, err := l.svcCtx.SysProductMemberModel.Insert(l.ctx, &productmember.SysProductMember{...}) if err != nil { if strings.Contains(err.Error(), "1062") || strings.Contains(err.Error(), "Duplicate entry") { return nil, response.ErrConflict("该用户已是该产品成员") } return nil, err } ``` --- ### 8. [Medium] JWT 中嵌入了完整权限列表,Token 体积随权限数增长 - **文件**:`internal/logic/auth/jwt.go` 第 23-40 行;`internal/middleware/jwtauthMiddleware.go` Claims 结构 - **描述**:`Claims.Perms` 字段将用户的所有权限 code 列表嵌入 access token。对于拥有数百个权限的用户,Token 体积会显著增长(可能超过 4KB),导致每次 HTTP 请求的 Header 过大。 - **影响**:增加网络传输开销;某些反向代理(如 Nginx 默认 4KB/8KB header buffer)可能拒绝过大的请求头。 - **建议**:Token 中仅保留 userId、productCode 等核心标识,权限列表通过 `UserDetailsLoader`(已有 Redis 缓存)在需要时加载。当前中间件已经通过 `loader.Load` 重新加载了完整用户信息,Token 中的 Perms 字段实际上未被中间件使用,仅用于前端展示。可以考虑在登录响应中单独返回 perms,从 Token 中移除。 --- ### 9. [Medium] RefreshToken 未限制刷新次数,旧 RefreshToken 可无限复用 - **文件**:`internal/logic/pub/refreshTokenLogic.go`;`internal/server/permserver.go` 第 162-200 行 - **描述**:`RefreshToken` 接口在签发新的 AccessToken 后,直接将原 RefreshToken 原样返回给客户端。RefreshToken 在有效期内可以被无限次调用,每次都会生成新的 AccessToken。 - **影响**:若 RefreshToken 泄漏,攻击者可在整个有效期内持续获取新的 AccessToken。通常的最佳实践是 Refresh Token Rotation——每次刷新时同时签发新的 RefreshToken 并使旧的失效。 - **建议**:当前通过 `tokenVersion` 机制可以在修改密码/冻结用户时使所有 token 失效,这提供了基本保障。若需更严格的安全性,可实现 Refresh Token Rotation(每次刷新签发新 RefreshToken),或在 Redis 中维护一个已使用 RefreshToken 的黑名单。 --- ### 10. [Low] 用户不存在时 JwtAuth 中间件返回误导性错误信息 - **文件**:`internal/middleware/jwtauthMiddleware.go` 第 74-78 行;`internal/loaders/userDetailsLoader.go` 第 129-133 行 - **描述**:当 JWT 有效但用户已被删除时,`UserDetailsLoader.Load` 返回一个默认的 `UserDetails`(Status=0)。中间件判断 `ud.Status != StatusEnabled` 后返回"账号已被冻结"。但实际上用户是不存在/已被删除。 - **影响**:用户收到错误的提示信息,可能导致困惑或误导排查方向。 - **建议**:在中间件中增加用户是否存在的判断: ```go ud := m.loader.Load(r.Context(), claims.UserId, claims.ProductCode) if ud.Username == "" { httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "用户不存在或已被删除")) return } if ud.Status != consts.StatusEnabled { httpx.ErrorCtx(r.Context(), w, response.NewCodeError(403, "账号已被冻结")) return } ``` --- ### 11. [Low] 缺少操作审计日志 - **描述**:当前系统在所有写操作(创建用户、绑定角色、修改权限、冻结用户、删除角色等)中未记录操作审计日志。仅有 go-zero 框架默认的请求日志。 - **影响**:当发生安全事件(如权限被篡改、用户被异常冻结)时,无法追溯是谁在什么时间执行了什么操作。对于权限管理系统,审计追溯能力是合规性的基本要求。 - **建议**:设计一个 `sys_audit_log` 表,记录 `operator_id`、`action`、`target_type`、`target_id`、`detail`、`ip`、`timestamp` 等字段。在关键业务逻辑中通过异步方式写入审计记录,避免影响主流程性能。 --- ### 12. [Low] DeptTree 和 ProductList 对非超管暴露全量数据 - **文件**:`internal/logic/dept/deptTreeLogic.go`;`internal/logic/product/productListLogic.go` - **描述**:`DeptTree` 接口无任何权限过滤,所有已登录用户可看到整个组织架构。`ProductList` 也对所有用户返回全部产品列表(AppKey 已对非超管隐藏)。 - **影响**:部门结构和产品列表属于组织内部信息,虽然不包含高敏感数据,但在安全要求较高的场景下可能不符合最小暴露原则。 - **建议**:根据实际业务需求评估是否需要按产品/部门范围过滤。如果当前业务场景下所有用户确实需要查看组织架构(如选择部门),可保持现状。 --- ### 13. [Low] UpdateDept 修改 deptType 时缺少影响评估 - **文件**:`internal/logic/dept/updateDeptLogic.go` - **描述**:将部门类型从 `DEV`(研发)改为 `NORMAL`,该部门下所有用户会立即失去"全权限"特权(因 `loadPerms` 中 DEV 部门自动获取全量权限的逻辑不再生效)。代码正确清除了缓存,但未在响应中提示此操作的影响范围。 - **影响**:超管执行部门类型变更后,该部门及子部门下的用户可能突然失去大量权限,如果未提前通知,可能导致业务中断。 - **建议**:在响应中返回受影响的用户数量,或在执行前增加确认机制。 --- ## 总结 | 级别 | 编号 | 问题 | 类型 | |------|------|------|------| | 🚩 High | #1 | UserList 数据越权泄漏 | 安全漏洞 | | 🚩 High | #2 | BindRoles 提权漏洞 | 安全漏洞 | | 🚩 High | #3 | appSecret 明文存储 | 安全漏洞 | | 🚩 High | #4 | UpdateUser 权限模型不一致 | 逻辑一致性 | | ⚠️ Medium | #5 | CreateUser 未加入产品成员 | 数据完整性 | | ⚠️ Medium | #6 | 缓存前缀全局可变状态 | 并发安全 | | ⚠️ Medium | #7 | AddMember 并发错误信息不友好 | 边界处理 | | ⚠️ Medium | #8 | JWT Token 体积过大风险 | 性能 | | ⚠️ Medium | #9 | RefreshToken 可无限复用 | 安全加固 | | ⚠️ Low | #10 | 用户不存在时错误信息误导 | 边界崩溃 | | ⚠️ Low | #11 | 缺少操作审计日志 | 安全合规 | | ⚠️ Low | #12 | DeptTree/ProductList 过度暴露 | 安全加固 | | ⚠️ Low | #13 | UpdateDept 修改类型缺少影响提示 | 健壮性 | **优先修复建议**:#1 和 #2 为最高优先级,直接影响数据安全和权限体系的可信度;#3 和 #4 建议在下一个迭代中修复。