审计时间:2026-04-17 审计范围:
/internal下所有非测试 Go 源文件(含 model、logic、handler、middleware、loaders、server 等)
internal/logic/user/bindRolesLogic.go:62DeleteByUserIdTx 删除了该用户在所有产品下的角色关联,随后仅重新插入当前产品的角色。DeleteByUserIdTx 的 SQL 为 DELETE FROM sys_user_role WHERE userId = ?,不带产品过滤。DeleteByUserIdTx 替换为已有的 DeleteByUserIdForProductTx,仅删除当前产品下的角色关联:// bindRolesLogic.go 事务内
if err := l.svcCtx.SysUserRoleModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil {
return err
}
internal/logic/user/setUserPermsLogic.go:69DeleteByUserIdTx 的 SQL 为 DELETE FROM sys_user_perm WHERE userId = ?,会删除用户在所有产品下的直授权记录,但随后仅重新插入当前产品的权限。DeleteByUserIdForProductTx:// setUserPermsLogic.go 事务内
if err := l.svcCtx.SysUserPermModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil {
return err
}
internal/logic/auth/access.go:88-99RequireProductAdmin 仅检查当前登录者的 MemberType 是否为 ADMIN,不校验该身份是否属于被操作的目标产品。调用者的 MemberType 来源于 JWT 中的 productCode(登录时的产品),而 CreateRole、BindRolePerms、DeleteRole、UpdateRole 等接口的 productCode/roleId 来自请求体,两者可能不一致。RequireProductAdminFor(ctx, targetProductCode) 函数,或在 RequireProductAdmin 中接受 targetProductCode 参数,与 caller 的产品上下文做比对:func RequireProductAdminFor(ctx context.Context, targetProductCode string) error {
caller := middleware.GetUserDetails(ctx)
if caller == nil {
return response.ErrUnauthorized("未登录")
}
if caller.IsSuperAdmin {
return nil
}
if caller.MemberType == consts.MemberTypeAdmin && caller.ProductCode == targetProductCode {
return nil
}
return response.ErrForbidden("仅超级管理员或该产品的管理员可执行此操作")
}
然后在 CreateRole、BindRolePerms、DeleteRole、UpdateRole 中使用目标产品的 productCode 调用此函数。
internal/logic/role/bindRolePermsLogic.go:31-65req.PermIds 中的权限是否属于该角色所在的产品。对比 setUserPermsLogic.go 中有做此验证(检查 p.ProductCode != productCode)。loadPerms 在计算用户权限时会按 productCode 过滤,不会直接导致越权执行,但会污染 sys_role_perm 表数据,导致角色详情返回不属于本产品的 permId,给前端和管理造成混乱。if len(req.PermIds) > 0 {
perms, err := l.svcCtx.SysPermModel.FindByIds(l.ctx, req.PermIds)
if err != nil {
return err
}
if len(perms) != len(req.PermIds) {
return response.ErrBadRequest("包含无效的权限ID")
}
for _, p := range perms {
if p.ProductCode != role.ProductCode {
return response.ErrBadRequest("不能绑定其他产品的权限")
}
}
}
internal/logic/pub/loginLogic.go:32-90、internal/server/permserver.go:112-159productCode 对应的产品是否存在且状态为启用product, err := l.svcCtx.SysProductModel.FindOneByCode(l.ctx, req.ProductCode)
if err != nil {
return nil, response.ErrBadRequest("产品不存在")
}
if product.Status != consts.StatusEnabled {
return nil, response.ErrForbidden("该产品已被禁用")
}
_, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(l.ctx, req.ProductCode, u.Id)
if memberErr != nil {
return nil, response.ErrForbidden("您不是该产品的成员")
}
etc/perm-api-dev.yaml、etc/perm-api-test.yaml、etc/perm-api-prod.yamlNsDmWyM@312)NsDmWyM@312)etc/*.yaml 加入 .gitignore,从 Git 历史中清除(git filter-branch 或 BFG)internal/logic/role/createRoleLogic.go:38、updateRoleLogic.go:42PermsLevel 字段无范围校验,可设为 0、负数或任意大整数。checkPermLevel 中通过 MinPermsLevel 数值比较来判定管理权限(数值越小权限越高),若不限制范围可能导致权限比较逻辑出现意外行为。if req.PermsLevel < 1 || req.PermsLevel > 999 {
return nil, response.ErrBadRequest("权限级别必须在 1-999 之间")
}
internal/logic/user/updateUserLogic.go:70-72DeptId 是否对应一个存在且启用的部门。若设置了不存在的 DeptId,后续 checkDeptHierarchy 中查询 SysDeptModel.FindOne(target.DeptId) 会失败,返回「无权操作」的误导性错误。if req.DeptId != nil && *req.DeptId > 0 {
if _, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, *req.DeptId); err != nil {
return response.ErrBadRequest("部门不存在")
}
}
internal/loaders/userDetailsLoader.go:175-189loadFromDB 内部各 loadXxx 方法在查询失败时仅打印日志,不返回错误。Load 方法通过 singleflight 调用 loadFromDB,始终返回 (ud, nil),即使用户不存在也会将不完整的 UserDetails(只有 userId 和 productCode,其余字段为零值)缓存到 Redis 并存活 5 分钟。Handle 中通过 ud.Status != consts.StatusEnabled 检查(StatusEnabled=1),而默认 Status=0,会导致用户被判定为「账号已被冻结」loadUser 失败时(特别是非 ErrNotFound 的错误)不应缓存结果,或设置更短的 TTL/不缓存:v, _, _ := l.sf.Do(key, func() (interface{}, error) {
ud, ok := l.loadFromDB(ctx, userId, productCode)
if ok {
// 正常缓存
if val, err := json.Marshal(ud); err == nil {
l.rds.SetexCtx(ctx, key, string(val), l.ttl)
}
}
// 加载失败时不缓存,让下次请求重试
return ud, nil
})
internal/logic/pub/adminLoginLogic.go:33-91adminLogin 接口仅验证 managementKey 后即允许任何用户登录管理后台,包括普通用户。虽然 managementKey 本身是一道屏障,但从设计意图来看管理后台应仅面向超管。if u.IsSuperAdmin != consts.IsSuperAdminYes {
return nil, response.ErrForbidden("管理后台仅限超级管理员登录")
}
internal/logic/user/createUserLogic.go:53-56FindOneByUsername 检查用户名是否存在,再执行 Insert。在两个操作之间,其他并发请求可能已经插入了同名用户。虽然代码在 Insert 失败时通过检查 "1062" / "Duplicate entry" 做了补偿处理,但这种双重检查模式产生了不必要的复杂度。FindOneByUsername 查询,直接依赖数据库唯一索引约束 + Duplicate entry 错误判断即可,减少一次 DB 查询。internal/server/permserver.go:112-159internal/logic/pub/refreshTokenLogic.go、internal/server/permserver.go:161-191RemoveMember),其已有的 refresh token 仍然可以续签新的 access token(最长 7 天 refresh token 有效期内)。RemoveMember 时将用户的 refresh token 加入黑名单。internal/logic/user/bindRolesLogic.go:69-78req.RoleIds 未做去重处理。若前端传入 [1, 1, 1],会在 sys_user_role 表中插入三条相同的记录(除非表有唯一联合索引)。req.RoleIds 去重:seen := make(map[int64]bool, len(req.RoleIds))
uniqueRoleIds := make([]int64, 0, len(req.RoleIds))
for _, id := range req.RoleIds {
if !seen[id] {
seen[id] = true
uniqueRoleIds = append(uniqueRoleIds, id)
}
}
internal/logic/user/setUserPermsLogic.go:49-66req.Perms 中若存在重复的 PermId,会插入多条记录。此外,对同一 PermId 若传入了不同 Effect(一个 ALLOW 一个 DENY),语义冲突但代码不会拦截。internal/logic/user/userListLogic.go、internal/logic/member/memberListLogic.go、internal/logic/role/roleListLogic.go 等productCode 参数的列表接口,校验调用者是否为该产品的成员或超管。internal/logic/user/userDetailLogic.go、internal/logic/role/roleDetailLogic.gointernal/logic/dept/deleteDeptLogic.go:33-47internal/logic/dept/createDeptLogic.go:50-53req.DeptType 为非空但非法值(如 "INVALID"),代码会直接使用该值(不经过枚举校验)。仅 updateDeptLogic.go 做了枚举校验。if deptType != consts.DeptTypeNormal && deptType != consts.DeptTypeDev {
return nil, response.ErrBadRequest("无效的部门类型")
}
internal/loaders/userDetailsLoader.go:153-171CleanByProduct 和 Clean 方法通过 Redis SCAN + pattern 匹配来批量删除缓存。在 Redis Cluster 模式下,SCAN 只扫描当前连接的节点,可能遗漏其他节点上的 key。当前配置为单节点 (Type: node),暂不受影响,但若未来迁移到 Cluster 需要注意。| 级别 | 编号 | 问题 | 影响面 |
|---|---|---|---|
| 🔴 High | H-1 | BindRoles 跨产品角色删除 | 数据丢失 |
| 🔴 High | H-2 | SetUserPerms 跨产品权限删除 | 数据丢失 |
| 🔴 High | H-3 | RequireProductAdmin 跨产品越权 | 越权操作 |
| 🔴 High | H-4 | BindRolePerms 未校验权限产品归属 | 数据污染 |
| 🔴 High | H-5 | Login 不验证产品/成员关系 | 未授权访问 |
| 🔴 High | H-6 | 敏感凭据明文提交 Git | 凭据泄露 |
| 🟡 Medium | M-1 | PermsLevel 无范围校验 | 逻辑异常 |
| 🟡 Medium | M-2 | UpdateUser 不验证 DeptId | 数据不一致 |
| 🟡 Medium | M-3 | UserDetailsLoader 缓存不完整数据 | 用户被误冻结 |
| 🟡 Medium | M-4 | AdminLogin 不限制超管 | 权限边界模糊 |
| 🟡 Medium | M-5 | CreateUser TOCTOU | 代码冗余 |
| 🟡 Medium | M-6 | gRPC Login 无速率限制 | 暴力破解风险 |
| 🟡 Medium | M-7 | RefreshToken 不验证成员关系 | token 续期漏洞 |
| 🟡 Medium | M-8 | BindRoles 未去重 | 数据重复 |
| 🟡 Medium | M-9 | SetUserPerms 未去重 | 语义冲突 |
| 🟢 Low | L-1 | 列表接口缺产品级权限控制 | 信息泄露 |
| 🟢 Low | L-2 | Detail 接口无访问控制 | 信息泄露 |
| 🟢 Low | L-3 | 部门删除无事务 | 理论竞态 |
| 🟢 Low | L-4 | DeptType 枚举校验不充分 | 脏数据 |
| 🟢 Low | L-5 | SCAN 在 Cluster 下的兼容性 | 缓存残留 |
优先建议修复顺序:H-1 → H-2 → H-3 → H-5 → H-6 → H-4 → M-3 → M-7 → 其余 Medium → Low