# 权限管理系统代码审计报告 > 审计时间:2026-04-17 > 审计范围:`/internal` 下所有非测试 Go 源文件(含 model、logic、handler、middleware、loaders、server 等) --- ## 🚩 核心逻辑漏洞 (High Risk) ### H-1: BindRoles 全量删除导致跨产品角色绑定丢失 - **文件**:`internal/logic/user/bindRolesLogic.go:62` - **描述**:为用户绑定角色时,事务中调用 `DeleteByUserIdTx` 删除了该用户在**所有产品**下的角色关联,随后仅重新插入当前产品的角色。`DeleteByUserIdTx` 的 SQL 为 `DELETE FROM sys_user_role WHERE userId = ?`,不带产品过滤。 - **影响**:产品 A 的管理员为某用户绑定角色时,该用户在产品 B、C 等其他产品中已有的角色绑定会被一并清除,导致跨产品数据丢失,权限异常。这是一个**静默数据破坏**——操作者和被操作者均不会收到任何告警。 - **修复方案**:将 `DeleteByUserIdTx` 替换为已有的 `DeleteByUserIdForProductTx`,仅删除当前产品下的角色关联: ```go // bindRolesLogic.go 事务内 if err := l.svcCtx.SysUserRoleModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil { return err } ``` --- ### H-2: SetUserPerms 全量删除导致跨产品用户直授权丢失 - **文件**:`internal/logic/user/setUserPermsLogic.go:69` - **描述**:与 H-1 同理,`DeleteByUserIdTx` 的 SQL 为 `DELETE FROM sys_user_perm WHERE userId = ?`,会删除用户在所有产品下的直授权记录,但随后仅重新插入当前产品的权限。 - **影响**:产品 A 管理员设置用户权限时,用户在产品 B 的 ALLOW/DENY 直授权被清除。 - **修复方案**:替换为 `DeleteByUserIdForProductTx`: ```go // setUserPermsLogic.go 事务内 if err := l.svcCtx.SysUserPermModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil { return err } ``` --- ### H-3: RequireProductAdmin 未校验目标产品归属,存在跨产品越权 - **文件**:`internal/logic/auth/access.go:88-99` - **描述**:`RequireProductAdmin` 仅检查当前登录者的 `MemberType` 是否为 `ADMIN`,**不校验该身份是否属于被操作的目标产品**。调用者的 `MemberType` 来源于 JWT 中的 `productCode`(登录时的产品),而 `CreateRole`、`BindRolePerms`、`DeleteRole`、`UpdateRole` 等接口的 `productCode`/`roleId` 来自请求体,两者可能不一致。 - **影响**:产品 A 的 ADMIN 登录后,可以: - 在产品 B 中创建/修改/删除角色 - 修改产品 B 角色的权限绑定 - 为产品 B 创建用户 - **修复方案**:新增 `RequireProductAdminFor(ctx, targetProductCode)` 函数,或在 `RequireProductAdmin` 中接受 `targetProductCode` 参数,与 caller 的产品上下文做比对: ```go 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 调用此函数。 --- ### H-4: BindRolePerms 不验证权限项归属于角色所在产品 - **文件**:`internal/logic/role/bindRolePermsLogic.go:31-65` - **描述**:为角色绑定权限时,仅验证角色存在,未校验 `req.PermIds` 中的权限是否属于该角色所在的产品。对比 `setUserPermsLogic.go` 中有做此验证(检查 `p.ProductCode != productCode`)。 - **影响**:攻击者可将产品 B 的权限 ID 绑定到产品 A 的角色上。虽然最终 `loadPerms` 在计算用户权限时会按 productCode 过滤,不会直接导致越权执行,但会污染 `sys_role_perm` 表数据,导致角色详情返回不属于本产品的 permId,给前端和管理造成混乱。 - **修复方案**:在事务之前增加权限归属校验: ```go 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("不能绑定其他产品的权限") } } } ``` --- ### H-5: Login/gRPC Login 不验证产品有效性和用户成员关系 - **文件**:`internal/logic/pub/loginLogic.go:32-90`、`internal/server/permserver.go:112-159` - **描述**:HTTP 和 gRPC 的 Login 接口均未验证: 1. `productCode` 对应的产品是否存在且状态为启用 2. 登录用户是否是该产品的成员 - **影响**: - 任何用户可用任意 productCode(甚至不存在的)获取有效的 JWT access token - 用户可为未加入的产品获取 token,虽然 perms 为空,但仍可访问仅需 JWT 认证的列表类接口(userList、roleList、permList、memberList 等),获得不应可见的数据 - **修复方案**:在密码验证通过后、生成 token 前,增加产品和成员校验: ```go 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("您不是该产品的成员") } ``` --- ### H-6: 敏感凭据明文提交到版本控制 - **文件**:`etc/perm-api-dev.yaml`、`etc/perm-api-test.yaml`、`etc/perm-api-prod.yaml` - **描述**:三个环境的配置文件均包含以下明文敏感信息并已提交到 Git: - MySQL 数据库密码(`NsDmWyM@312`) - Redis 密码(`NsDmWyM@312`) - JWT AccessSecret / RefreshSecret - ManagementKey - 且三个环境**共用同一套 MySQL 和 Redis 密码** - **影响**: - 任何有代码仓库读权限的人均可获取全部环境的数据库和 Redis 凭据 - 密码一旦泄露,三个环境同时受影响 - JWT secret 泄露后可伪造任意用户的 token - **修复方案**: 1. 将 `etc/*.yaml` 加入 `.gitignore`,从 Git 历史中清除(`git filter-branch` 或 BFG) 2. 使用环境变量或密钥管理服务(如 Vault、AWS Secrets Manager)注入敏感配置 3. 立即轮换所有已泄露的密码和 secret 4. 为 dev/test/prod 使用不同的凭据 --- ## ⚠️ 健壮性与性能建议 (Medium/Low) ### M-1: CreateRole/UpdateRole 未校验 PermsLevel 范围 - **文件**:`internal/logic/role/createRoleLogic.go:38`、`updateRoleLogic.go:42` - **描述**:`PermsLevel` 字段无范围校验,可设为 0、负数或任意大整数。`checkPermLevel` 中通过 `MinPermsLevel` 数值比较来判定管理权限(数值越小权限越高),若不限制范围可能导致权限比较逻辑出现意外行为。 - **建议**:增加 PermsLevel 的合法范围校验(如 1-999),确保符合业务预期: ```go if req.PermsLevel < 1 || req.PermsLevel > 999 { return nil, response.ErrBadRequest("权限级别必须在 1-999 之间") } ``` --- ### M-2: UpdateUser 不验证 DeptId 有效性 - **文件**:`internal/logic/user/updateUserLogic.go:70-72` - **描述**:超级管理员修改用户部门时,不检查 `DeptId` 是否对应一个存在且启用的部门。若设置了不存在的 DeptId,后续 `checkDeptHierarchy` 中查询 `SysDeptModel.FindOne(target.DeptId)` 会失败,返回「无权操作」的误导性错误。 - **建议**: ```go if req.DeptId != nil && *req.DeptId > 0 { if _, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, *req.DeptId); err != nil { return response.ErrBadRequest("部门不存在") } } ``` --- ### M-3: UserDetailsLoader.loadFromDB 静默吞错误,可能缓存不完整数据 - **文件**:`internal/loaders/userDetailsLoader.go:175-189` - **描述**:`loadFromDB` 内部各 `loadXxx` 方法在查询失败时仅打印日志,不返回错误。`Load` 方法通过 `singleflight` 调用 `loadFromDB`,始终返回 `(ud, nil)`,即使用户不存在也会将不完整的 `UserDetails`(只有 userId 和 productCode,其余字段为零值)缓存到 Redis 并存活 5 分钟。 - **影响**: - 若 DB 瞬时故障,所有并发请求将获取并缓存空数据,在 TTL 内该用户无法正常使用 - 中间件 `Handle` 中通过 `ud.Status != consts.StatusEnabled` 检查(StatusEnabled=1),而默认 Status=0,会导致用户被判定为「账号已被冻结」 - **建议**:在 `loadUser` 失败时(特别是非 ErrNotFound 的错误)不应缓存结果,或设置更短的 TTL/不缓存: ```go 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 }) ``` --- ### M-4: AdminLogin 不限制仅超管用户登录 - **文件**:`internal/logic/pub/adminLoginLogic.go:33-91` - **描述**:`adminLogin` 接口仅验证 `managementKey` 后即允许任何用户登录管理后台,包括普通用户。虽然 `managementKey` 本身是一道屏障,但从设计意图来看管理后台应仅面向超管。 - **建议**:如管理后台仅面向超管,可在密码验证后增加: ```go if u.IsSuperAdmin != consts.IsSuperAdminYes { return nil, response.ErrForbidden("管理后台仅限超级管理员登录") } ``` --- ### M-5: CreateUser 用户名唯一性存在 TOCTOU 窗口 - **文件**:`internal/logic/user/createUserLogic.go:53-56` - **描述**:先通过 `FindOneByUsername` 检查用户名是否存在,再执行 `Insert`。在两个操作之间,其他并发请求可能已经插入了同名用户。虽然代码在 Insert 失败时通过检查 `"1062"` / `"Duplicate entry"` 做了补偿处理,但这种双重检查模式产生了不必要的复杂度。 - **建议**:可以去掉前置的 `FindOneByUsername` 查询,直接依赖数据库唯一索引约束 + `Duplicate entry` 错误判断即可,减少一次 DB 查询。 --- ### M-6: gRPC Login 可用于暴力枚举用户密码 - **文件**:`internal/server/permserver.go:112-159` - **描述**:gRPC Login 接口无速率限制。在内部网络中,攻击者可对 gRPC 端口发起高速暴力破解(bcrypt 有计算成本,但大量并发仍可造成 CPU 压力和逐步猜测)。HTTP 侧通常有 Nginx/WAF 保护,但 gRPC 端口可能缺少此层防护。 - **建议**: - 在 gRPC server 添加速率限制 interceptor - 或确保 gRPC 端口不对外暴露,仅内部服务间调用 --- ### M-7: RefreshToken 不验证用户是否仍为产品成员 - **文件**:`internal/logic/pub/refreshTokenLogic.go`、`internal/server/permserver.go:161-191` - **描述**:RefreshToken 接口仅检查用户状态是否启用,不检查用户是否仍为该产品的成员。若用户被移除出产品成员(`RemoveMember`),其已有的 refresh token 仍然可以续签新的 access token(最长 7 天 refresh token 有效期内)。 - **建议**:在 refresh 时增加成员关系检查,或在 `RemoveMember` 时将用户的 refresh token 加入黑名单。 --- ### M-8: BindRoles 中角色 ID 去重缺失 - **文件**:`internal/logic/user/bindRolesLogic.go:69-78` - **描述**:`req.RoleIds` 未做去重处理。若前端传入 `[1, 1, 1]`,会在 `sys_user_role` 表中插入三条相同的记录(除非表有唯一联合索引)。 - **建议**:在插入前对 `req.RoleIds` 去重: ```go 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) } } ``` --- ### M-9: SetUserPerms 中 PermId 去重缺失 - **文件**:`internal/logic/user/setUserPermsLogic.go:49-66` - **描述**:同 M-8,`req.Perms` 中若存在重复的 PermId,会插入多条记录。此外,对同一 PermId 若传入了不同 Effect(一个 ALLOW 一个 DENY),语义冲突但代码不会拦截。 - **建议**:对 PermId 去重,并检查同一 PermId 不能同时为 ALLOW 和 DENY。 --- ### L-1: UserList/MemberList/RoleList 等列表接口缺少产品级权限控制 - **文件**:`internal/logic/user/userListLogic.go`、`internal/logic/member/memberListLogic.go`、`internal/logic/role/roleListLogic.go` 等 - **描述**:所有列表类接口仅需 JWT 认证即可访问,无产品成员身份校验。已登录的任何用户可查看任意产品的角色列表、权限列表、成员列表。结合 H-5(Login 不验证成员关系),用户只需注册并使用任意 productCode 登录,即可遍历所有产品的内部数据。 - **建议**:对需要 `productCode` 参数的列表接口,校验调用者是否为该产品的成员或超管。 --- ### L-2: UserDetail/RoleDetail 未校验调用者是否有权查看 - **文件**:`internal/logic/user/userDetailLogic.go`、`internal/logic/role/roleDetailLogic.go` - **描述**:任何已认证用户可通过 ID 查看任意用户或角色的详情,包括用户的邮箱、手机号等个人信息。 - **建议**:根据业务需求,考虑限制用户仅能查看本产品范围内的数据,或对敏感字段脱敏。 --- ### L-3: 部门删除缺少事务保护 - **文件**:`internal/logic/dept/deleteDeptLogic.go:33-47` - **描述**:删除部门前先查子部门和关联用户,确认为空后再删除。但在检查和删除之间(虽然是极短窗口),理论上可能有新数据插入。在实际业务中,部门操作频率极低且限超管,此风险可接受。 - **建议**:如追求严格一致性,可将检查与删除放入同一事务中,并在 SQL 层面做最终校验。 --- ### L-4: DeptType 枚举校验不充分 - **文件**:`internal/logic/dept/createDeptLogic.go:50-53` - **描述**:创建部门时,若 `req.DeptType` 为非空但非法值(如 `"INVALID"`),代码会直接使用该值(不经过枚举校验)。仅 `updateDeptLogic.go` 做了枚举校验。 - **建议**:创建时也增加枚举校验: ```go if deptType != consts.DeptTypeNormal && deptType != consts.DeptTypeDev { return nil, response.ErrBadRequest("无效的部门类型") } ``` --- ### L-5: cleanByPattern 使用 SCAN 在 Redis Cluster 下可能遗漏 key - **文件**:`internal/loaders/userDetailsLoader.go:153-171` - **描述**:`CleanByProduct` 和 `Clean` 方法通过 Redis `SCAN` + pattern 匹配来批量删除缓存。在 Redis Cluster 模式下,`SCAN` 只扫描当前连接的节点,可能遗漏其他节点上的 key。当前配置为单节点 (`Type: node`),暂不受影响,但若未来迁移到 Cluster 需要注意。 - **建议**:预留 Cluster 兼容方案,或在 key 设计中使用 hash tag 确保同一用户的 key 落在同一 slot。 --- ## 📋 审计总结 | 级别 | 编号 | 问题 | 影响面 | |------|------|------|--------| | 🔴 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