|
@@ -1,342 +1,476 @@
|
|
|
-# 权限管理系统代码审计报告
|
|
|
|
|
|
|
+# 权限管理系统 (perms-system-server) 深度代码审计报告
|
|
|
|
|
|
|
|
> 审计时间:2026-04-17
|
|
> 审计时间:2026-04-17
|
|
|
-> 审计范围:`/internal` 下所有非测试 Go 源文件(含 model、logic、handler、middleware、loaders、server 等)
|
|
|
|
|
|
|
+> 审计范围:全部非测试业务源代码(logic / model / middleware / loaders / server / handler)
|
|
|
|
|
+> 审计维度:逻辑一致性、并发与竞态、资源管理、数据完整性、安全漏洞、边界崩溃
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 🚩 核心逻辑漏洞 (High Risk)
|
|
## 🚩 核心逻辑漏洞 (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`,仅删除当前产品下的角色关联:
|
|
|
|
|
|
|
+### H-1:UserDetailsLoader.loadPerms 跨产品权限泄漏
|
|
|
|
|
|
|
|
-```go
|
|
|
|
|
-// bindRolesLogic.go 事务内
|
|
|
|
|
-if err := l.svcCtx.SysUserRoleModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+- **描述**:`internal/loaders/userDetailsLoader.go` 第 329-330 行,在为普通成员加载权限时,`FindPermIdsByUserIdAndEffect` 查询的 SQL 是 `SELECT permId FROM sys_user_perm WHERE userId = ? AND effect = ?`,该查询**没有按 productCode 过滤**。如果一个用户是多个产品的成员,且在不同产品下都被设置了用户级权限(ALLOW/DENY),则加载产品 A 的上下文时,会把产品 B 的用户级权限也混入计算。
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+ 具体污染路径:
|
|
|
|
|
+ 1. 用户在产品 B 下有 ALLOW 权限 `permId=100`(code 为 `order:delete`)
|
|
|
|
|
+ 2. 当加载产品 A 的上下文时,`allowIds` 包含 `permId=100`
|
|
|
|
|
+ 3. `permId=100` 被加入 `permIdSet`,最终 `FindByIds` 返回时没有过滤 `productCode`
|
|
|
|
|
+ 4. 产品 B 的权限 code `order:delete` 泄漏到产品 A 的 `Perms` 列表和 JWT Token 中
|
|
|
|
|
|
|
|
-### H-2: SetUserPerms 全量删除导致跨产品用户直授权丢失
|
|
|
|
|
|
|
+ 更严重的是:产品 B 中的 DENY 规则也会泄漏,可能**错误阻止**产品 A 中本应拥有的权限。
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:
|
|
|
|
|
+ - 跨产品权限授予:用户在未授权的产品中获得额外权限
|
|
|
|
|
+ - 跨产品权限拒绝:产品 B 的 DENY 规则错误地屏蔽产品 A 的合法权限
|
|
|
|
|
+ - JWT Token 中包含其他产品的权限信息(信息泄漏)
|
|
|
|
|
|
|
|
-- **文件**:`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
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+ 方案一(推荐):在 `loadPerms` 中查询时增加产品过滤
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ // internal/loaders/userDetailsLoader.go - loadPerms 方法中
|
|
|
|
|
+ // 修改为通过子查询限定 productCode
|
|
|
|
|
+ allowIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(
|
|
|
|
|
+ ctx, ud.UserId, consts.PermEffectAllow, ud.ProductCode)
|
|
|
|
|
+ denyIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(
|
|
|
|
|
+ ctx, ud.UserId, consts.PermEffectDeny, ud.ProductCode)
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+ 对应新增 Model 方法:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ func (m *customSysUserPermModel) FindPermIdsByUserIdAndEffectForProduct(
|
|
|
|
|
+ ctx context.Context, userId int64, effect string, productCode string,
|
|
|
|
|
+ ) ([]int64, error) {
|
|
|
|
|
+ var ids []int64
|
|
|
|
|
+ query := fmt.Sprintf(
|
|
|
|
|
+ "SELECT up.`permId` FROM %s up "+
|
|
|
|
|
+ "INNER JOIN `sys_perm` p ON up.`permId` = p.`id` "+
|
|
|
|
|
+ "WHERE up.`userId` = ? AND up.`effect` = ? AND p.`productCode` = ?",
|
|
|
|
|
+ m.table)
|
|
|
|
|
+ if err := m.QueryRowsNoCacheCtx(ctx, &ids, query, userId, effect, productCode); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return ids, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### 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("仅超级管理员或该产品的管理员可执行此操作")
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### H-2:UpdateUser 接口可绕过超管状态保护
|
|
|
|
|
|
|
|
-然后在 `CreateRole`、`BindRolePerms`、`DeleteRole`、`UpdateRole` 中使用目标产品的 productCode 调用此函数。
|
|
|
|
|
|
|
+- **描述**:`internal/logic/user/updateUserStatusLogic.go` 明确禁止修改超级管理员的状态:
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ if user.IsSuperAdmin == consts.IsSuperAdminYes {
|
|
|
|
|
+ return response.ErrForbidden("不能修改超级管理员的状态")
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-### H-4: BindRolePerms 不验证权限项归属于角色所在产品
|
|
|
|
|
|
|
+ 但 `internal/logic/user/updateUserLogic.go` 中也接受 `Status` 字段,对于非本人操作只要求调用者是超管,**没有检查目标用户是否也是超管**:
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/role/bindRolePermsLogic.go:31-65`
|
|
|
|
|
-- **描述**:为角色绑定权限时,仅验证角色存在,未校验 `req.PermIds` 中的权限是否属于该角色所在的产品。对比 `setUserPermsLogic.go` 中有做此验证(检查 `p.ProductCode != productCode`)。
|
|
|
|
|
-- **影响**:攻击者可将产品 B 的权限 ID 绑定到产品 A 的角色上。虽然最终 `loadPerms` 在计算用户权限时会按 productCode 过滤,不会直接导致越权执行,但会污染 `sys_role_perm` 表数据,导致角色详情返回不属于本产品的 permId,给前端和管理造成混乱。
|
|
|
|
|
-- **修复方案**:在事务之前增加权限归属校验:
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if !caller.IsSuperAdmin {
|
|
|
|
|
+ return response.ErrForbidden("仅允许修改自己的信息或超管操作")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // ... 后续直接设置 status,无超管保护
|
|
|
|
|
+ if req.Status == consts.StatusEnabled || req.Status == consts.StatusDisabled {
|
|
|
|
|
+ user.Status = req.Status
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-```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("不能绑定其他产品的权限")
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+ 攻击路径:超管 A 通过 `POST /api/user/update` 传入 `{"id": <超管B的ID>, "status": 2}`,即可冻结超管 B,绕过 `updateUserStatus` 的保护逻辑。
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+- **影响**:超级管理员之间可互相冻结账号,破坏系统管理根基。在最坏场景下,攻击者获取任一超管账号后可瘫痪所有其他超管。
|
|
|
|
|
|
|
|
-### H-5: Login/gRPC Login 不验证产品有效性和用户成员关系
|
|
|
|
|
|
|
+- **修复方案**:在 `updateUserLogic.go` 中增加超管保护检查:
|
|
|
|
|
|
|
|
-- **文件**:`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("您不是该产品的成员")
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ if caller.UserId != req.Id {
|
|
|
|
|
+ if !caller.IsSuperAdmin {
|
|
|
|
|
+ return response.ErrForbidden("仅允许修改自己的信息或超管操作")
|
|
|
|
|
+ }
|
|
|
|
|
+ // 新增:禁止通过此接口修改其他超管的状态
|
|
|
|
|
+ if req.Status != 0 && user.IsSuperAdmin == consts.IsSuperAdminYes {
|
|
|
|
|
+ return response.ErrForbidden("不能通过此接口修改超级管理员的状态")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### H-6: 敏感凭据明文提交到版本控制
|
|
|
|
|
|
|
+### H-3:产品成员被禁用后仍可正常登录
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`sys_product_member` 表有 `status` 字段(1=启用,2=禁用),但登录流程和 UserDetailsLoader 均未校验此字段。
|
|
|
|
|
+
|
|
|
|
|
+ - `loginLogic.go` 第 67 行仅检查成员是否存在,不检查 status:
|
|
|
|
|
+ ```go
|
|
|
|
|
+ if _, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(
|
|
|
|
|
+ l.ctx, req.ProductCode, u.Id); memberErr != nil {
|
|
|
|
|
+ return nil, response.ErrForbidden("您不是该产品的成员")
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+ - `permserver.go` 第 140 行 gRPC Login 同样如此
|
|
|
|
|
+ - `userDetailsLoader.go` 的 `loadMembership` 方法也不检查 `member.Status`
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:管理员将某个成员禁用后,该成员仍然可以正常登录、获取 Token、使用该产品的所有权限,禁用操作形同虚设。
|
|
|
|
|
|
|
|
-- **文件**:`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 使用不同的凭据
|
|
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+ 在 `loginLogic.go` 和 `permserver.go` 的 Login 中增加成员状态检查:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ member, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(
|
|
|
|
|
+ l.ctx, req.ProductCode, u.Id)
|
|
|
|
|
+ if memberErr != nil {
|
|
|
|
|
+ return nil, response.ErrForbidden("您不是该产品的成员")
|
|
|
|
|
+ }
|
|
|
|
|
+ if member.Status != consts.StatusEnabled {
|
|
|
|
|
+ return nil, response.ErrForbidden("您在该产品下的成员资格已被禁用")
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-## ⚠️ 健壮性与性能建议 (Medium/Low)
|
|
|
|
|
|
|
+ 在 `userDetailsLoader.go` 的 `loadMembership` 中增加状态检查:
|
|
|
|
|
|
|
|
-### M-1: CreateRole/UpdateRole 未校验 PermsLevel 范围
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ if member.Status != consts.StatusEnabled {
|
|
|
|
|
+ return // 禁用的成员视为无成员身份
|
|
|
|
|
+ }
|
|
|
|
|
+ ud.MemberType = member.MemberType
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-- **文件**:`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 之间")
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+### H-4:gRPC VerifyToken 不检查用户实时状态
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`internal/server/permserver.go` 的 `VerifyToken` 方法仅验证 JWT 签名和过期时间,不检查用户当前是否仍然处于启用状态:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ func (s *PermServer) VerifyToken(ctx context.Context, req *pb.VerifyTokenReq) (*pb.VerifyTokenResp, error) {
|
|
|
|
|
+ token, err := jwt.ParseWithClaims(req.AccessToken, &middleware.Claims{}, ...)
|
|
|
|
|
+ if err != nil || !token.Valid {
|
|
|
|
|
+ return &pb.VerifyTokenResp{Valid: false}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ claims, ok := token.Claims.(*middleware.Claims)
|
|
|
|
|
+ if !ok || claims.TokenType != consts.TokenTypeAccess {
|
|
|
|
|
+ return &pb.VerifyTokenResp{Valid: false}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ // 直接返回 valid=true,未检查用户实时状态
|
|
|
|
|
+ return &pb.VerifyTokenResp{Valid: true, ...}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+ 而 HTTP 端的 JWT 中间件(`jwtauthMiddleware.go`)会通过 `UserDetailsLoader.Load` 实时检查用户状态。
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:当用户被冻结或从产品中移除后,如果其他微服务通过 gRPC `VerifyToken` 接口验证 Token,仍会得到 `Valid: true`,导致已冻结用户继续使用系统直到 Token 过期(默认 2 小时)。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:在 `VerifyToken` 中增加实时状态检查:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ func (s *PermServer) VerifyToken(ctx context.Context, req *pb.VerifyTokenReq) (*pb.VerifyTokenResp, error) {
|
|
|
|
|
+ // ... JWT 签名验证保持不变 ...
|
|
|
|
|
+
|
|
|
|
|
+ // 新增:加载用户实时状态
|
|
|
|
|
+ ud := s.svcCtx.UserDetailsLoader.Load(ctx, claims.UserId, claims.ProductCode)
|
|
|
|
|
+ if ud.Status != consts.StatusEnabled {
|
|
|
|
|
+ return &pb.VerifyTokenResp{Valid: false}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ if claims.ProductCode != "" && !ud.IsSuperAdmin && ud.MemberType == "" {
|
|
|
|
|
+ return &pb.VerifyTokenResp{Valid: false}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return &pb.VerifyTokenResp{
|
|
|
|
|
+ Valid: true,
|
|
|
|
|
+ UserId: claims.UserId,
|
|
|
|
|
+ Username: claims.Username,
|
|
|
|
|
+ MemberType: ud.MemberType, // 使用实时数据而非 Token 中的缓存数据
|
|
|
|
|
+ Perms: ud.Perms,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-2: UpdateUser 不验证 DeptId 有效性
|
|
|
|
|
|
|
+### H-5:gRPC Login 端点无速率限制
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:HTTP 登录端口通过 `LoginRateLimit` 中间件进行速率限制(60 秒内最多 20 次),但 gRPC 端口的 `Login` 方法(`permserver.go` 第 112 行)**没有任何速率限制**。
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ // routes.go - HTTP Login 有速率限制
|
|
|
|
|
+ rest.WithMiddlewares(
|
|
|
|
|
+ []rest.Middleware{serverCtx.LoginRateLimit},
|
|
|
|
|
+ []rest.Route{
|
|
|
|
|
+ {Method: http.MethodPost, Path: "/auth/login", Handler: pub.LoginHandler(serverCtx)},
|
|
|
|
|
+ }...,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // permserver.go - gRPC Login 无速率限制
|
|
|
|
|
+ func (s *PermServer) Login(ctx context.Context, req *pb.LoginReq) (*pb.LoginResp, error) {
|
|
|
|
|
+ // 直接进入业务逻辑,无任何限流
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:攻击者可通过 gRPC 端口对登录接口进行高频暴力破解,绕过 HTTP 的速率限制防护。由于使用 bcrypt 校验密码,高并发攻击还可能导致 CPU 资源耗尽。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:为 gRPC 服务添加 UnaryInterceptor 形式的速率限制,或在 `Login` 方法入口处增加限流逻辑:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ func (s *PermServer) Login(ctx context.Context, req *pb.LoginReq) (*pb.LoginResp, error) {
|
|
|
|
|
+ // 使用 peer 获取客户端 IP 进行限流
|
|
|
|
|
+ p, _ := peer.FromContext(ctx)
|
|
|
|
|
+ ip := p.Addr.String()
|
|
|
|
|
+ code, _ := s.limiter.Take(fmt.Sprintf("grpc:login:%s", ip))
|
|
|
|
|
+ if code == limit.OverQuota {
|
|
|
|
|
+ return nil, status.Error(codes.ResourceExhausted, "请求过于频繁")
|
|
|
|
|
+ }
|
|
|
|
|
+ // ... 原有逻辑 ...
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-- **文件**:`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("部门不存在")
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+## ⚠️ 健壮性与性能建议 (Medium)
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-3: UserDetailsLoader.loadFromDB 静默吞错误,可能缓存不完整数据
|
|
|
|
|
|
|
+### M-1:Rate Limiter IP 提取可被伪造
|
|
|
|
|
|
|
|
-- **文件**:`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
|
|
|
|
|
-})
|
|
|
|
|
-```
|
|
|
|
|
|
|
+- **描述**:`ratelimitMiddleware.go` 按 `X-Forwarded-For → X-Real-IP → RemoteAddr` 顺序提取客户端 IP:
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ ip := r.Header.Get("X-Forwarded-For")
|
|
|
|
|
+ if ip == "" {
|
|
|
|
|
+ ip = r.Header.Get("X-Real-IP")
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-### M-4: AdminLogin 不限制仅超管用户登录
|
|
|
|
|
|
|
+ `X-Forwarded-For` 头可以被客户端直接伪造。如果服务不在可信反向代理后面,攻击者可以在每次请求中设置不同的 `X-Forwarded-For` 值来绕过速率限制。
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/pub/adminLoginLogic.go:33-91`
|
|
|
|
|
-- **描述**:`adminLogin` 接口仅验证 `managementKey` 后即允许任何用户登录管理后台,包括普通用户。虽然 `managementKey` 本身是一道屏障,但从设计意图来看管理后台应仅面向超管。
|
|
|
|
|
-- **建议**:如管理后台仅面向超管,可在密码验证后增加:
|
|
|
|
|
|
|
+- **影响**:登录接口的速率限制可被绕过,使暴力破解攻击成为可能。
|
|
|
|
|
|
|
|
-```go
|
|
|
|
|
-if u.IsSuperAdmin != consts.IsSuperAdminYes {
|
|
|
|
|
- return nil, response.ErrForbidden("管理后台仅限超级管理员登录")
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+- **修复方案**:如果部署在反向代理后,应提取 `X-Forwarded-For` 中的**第一个非信任 IP**;如果直接对外暴露,应优先使用 `RemoteAddr`。建议根据部署拓扑配置信任代理列表:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ ip := extractRealIP(r, trustedProxies)
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-5: CreateUser 用户名唯一性存在 TOCTOU 窗口
|
|
|
|
|
|
|
+### M-2:RefreshToken 端点无速率限制
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`/api/auth/refreshToken` 在 `routes.go` 中注册时既无 JWT 认证中间件,也无速率限制中间件。
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ server.AddRoutes(
|
|
|
|
|
+ []rest.Route{
|
|
|
|
|
+ {Method: http.MethodPost, Path: "/auth/refreshToken", Handler: pub.RefreshTokenHandler(serverCtx)},
|
|
|
|
|
+ {Method: http.MethodPost, Path: "/perm/sync", Handler: pub.SyncPermsHandler(serverCtx)},
|
|
|
|
|
+ },
|
|
|
|
|
+ rest.WithPrefix("/api"),
|
|
|
|
|
+ )
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/user/createUserLogic.go:53-56`
|
|
|
|
|
-- **描述**:先通过 `FindOneByUsername` 检查用户名是否存在,再执行 `Insert`。在两个操作之间,其他并发请求可能已经插入了同名用户。虽然代码在 Insert 失败时通过检查 `"1062"` / `"Duplicate entry"` 做了补偿处理,但这种双重检查模式产生了不必要的复杂度。
|
|
|
|
|
-- **建议**:可以去掉前置的 `FindOneByUsername` 查询,直接依赖数据库唯一索引约束 + `Duplicate entry` 错误判断即可,减少一次 DB 查询。
|
|
|
|
|
|
|
+- **影响**:如果 RefreshToken 泄漏,攻击者可以无限次调用此端点,持续生成新的 AccessToken,且无法通过限流缓解。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:为 RefreshToken 端点增加独立的速率限制,基于 token 中的 userId 进行限流。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-6: gRPC Login 可用于暴力枚举用户密码
|
|
|
|
|
|
|
+### M-3:部门禁用后不影响其下用户的登录和权限
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`sys_dept` 表有 `status` 字段,但整个系统中没有任何地方在加载用户信息时检查其所属部门是否处于启用状态。
|
|
|
|
|
+
|
|
|
|
|
+ - `userDetailsLoader.go` 的 `loadDept` 不检查部门状态
|
|
|
|
|
+ - `loadPerms` 中依赖 `ud.DeptType == consts.DeptTypeDev` 判断是否授予全量权限,也不检查部门是否被禁用
|
|
|
|
|
+ - 一个被禁用的 DEV 类型部门下的用户仍然会获得该产品的全部权限
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:管理员禁用部门后,预期该部门下用户的权限应受到影响(至少 DEV 部门的自动全权限应该失效),但实际上没有任何效果。
|
|
|
|
|
|
|
|
-- **文件**:`internal/server/permserver.go:112-159`
|
|
|
|
|
-- **描述**:gRPC Login 接口无速率限制。在内部网络中,攻击者可对 gRPC 端口发起高速暴力破解(bcrypt 有计算成本,但大量并发仍可造成 CPU 压力和逐步猜测)。HTTP 侧通常有 Nginx/WAF 保护,但 gRPC 端口可能缺少此层防护。
|
|
|
|
|
-- **建议**:
|
|
|
|
|
- - 在 gRPC server 添加速率限制 interceptor
|
|
|
|
|
- - 或确保 gRPC 端口不对外暴露,仅内部服务间调用
|
|
|
|
|
|
|
+- **修复方案**:在 `loadPerms` 中判断 `DeptType` 时,增加部门状态检查:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ if ud.IsSuperAdmin ||
|
|
|
|
|
+ ud.MemberType == consts.MemberTypeAdmin ||
|
|
|
|
|
+ ud.MemberType == consts.MemberTypeDeveloper ||
|
|
|
|
|
+ (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
|
|
|
|
|
+ // 全量权限
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-7: RefreshToken 不验证用户是否仍为产品成员
|
|
|
|
|
|
|
+### M-4:Redis SCAN 在 Cluster 模式下不兼容
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`userDetailsLoader.go` 的 `cleanByPattern` 使用 `SCAN` 命令按通配符模式清除缓存。代码注释已标注此问题,但仍是一个部署风险。
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ // NOTE: SCAN only works on single-node Redis. For Redis Cluster, consider using hash tags
|
|
|
|
|
+ func (l *UserDetailsLoader) cleanByPattern(ctx context.Context, pattern string) {
|
|
|
|
|
+ var cursor uint64
|
|
|
|
|
+ for {
|
|
|
|
|
+ keys, cur, err := l.rds.ScanCtx(ctx, cursor, pattern, 100)
|
|
|
|
|
+ // ...
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+ 此方法被 `Clean`(清除某用户所有产品缓存)和 `CleanByProduct`(清除某产品所有用户缓存)调用。
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/pub/refreshTokenLogic.go`、`internal/server/permserver.go:161-191`
|
|
|
|
|
-- **描述**:RefreshToken 接口仅检查用户状态是否启用,不检查用户是否仍为该产品的成员。若用户被移除出产品成员(`RemoveMember`),其已有的 refresh token 仍然可以续签新的 access token(最长 7 天 refresh token 有效期内)。
|
|
|
|
|
-- **建议**:在 refresh 时增加成员关系检查,或在 `RemoveMember` 时将用户的 refresh token 加入黑名单。
|
|
|
|
|
|
|
+- **影响**:如果未来 Redis 切换为 Cluster 模式,`SCAN` 只能在单个节点上执行,无法跨节点匹配 key,导致缓存无法正确失效,引发权限数据过期不清除问题。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:使用 Redis Hash Tag `{tag}` 将相关 key 路由到同一 slot,或改用主动记录关联 key 的方式(如维护一个 Set 记录用户关联的所有缓存 key),清除时通过 Set 成员精确删除。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-8: BindRoles 中角色 ID 去重缺失
|
|
|
|
|
|
|
+### M-5:HTTP 与 gRPC 登录逻辑高度重复
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/user/bindRolesLogic.go:69-78`
|
|
|
|
|
-- **描述**:`req.RoleIds` 未做去重处理。若前端传入 `[1, 1, 1]`,会在 `sys_user_role` 表中插入三条相同的记录(除非表有唯一联合索引)。
|
|
|
|
|
-- **建议**:在插入前对 `req.RoleIds` 去重:
|
|
|
|
|
|
|
+- **描述**:`loginLogic.go` 和 `permserver.go Login` 中的登录逻辑几乎完全相同(校验用户名密码、检查状态、检查产品、检查成员关系、生成 Token),但分别独立实现。
|
|
|
|
|
|
|
|
-```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)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+- **影响**:当修复上述 H-3(成员状态检查)等问题时,需要同时修改两处代码,容易遗漏。未来任何登录逻辑的变更都需要双重维护。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:将核心登录逻辑抽取为共享的 service 方法,HTTP handler 和 gRPC server 都调用同一个方法。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-9: SetUserPerms 中 PermId 去重缺失
|
|
|
|
|
|
|
+## ⚠️ 健壮性与性能建议 (Low)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### L-1:SyncPerms 接口无速率限制
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`/api/perm/sync` 端点无任何中间件保护(无 JWT、无 RateLimit),仅靠 `appKey + appSecret` 认证。虽然密钥泄漏的概率较低,但一旦泄漏,攻击者可以高频调用此接口触发大量数据库写操作和缓存清除。
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/user/setUserPermsLogic.go:49-66`
|
|
|
|
|
-- **描述**:同 M-8,`req.Perms` 中若存在重复的 PermId,会插入多条记录。此外,对同一 PermId 若传入了不同 Effect(一个 ALLOW 一个 DENY),语义冲突但代码不会拦截。
|
|
|
|
|
-- **建议**:对 PermId 去重,并检查同一 PermId 不能同时为 ALLOW 和 DENY。
|
|
|
|
|
|
|
+- **修复方案**:为 SyncPerms 增加基于 appKey 的速率限制。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### L-1: UserList/MemberList/RoleList 等列表接口缺少产品级权限控制
|
|
|
|
|
|
|
+### L-2:CreateProduct 响应暴露 AppSecret 明文
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/user/userListLogic.go`、`internal/logic/member/memberListLogic.go`、`internal/logic/role/roleListLogic.go` 等
|
|
|
|
|
-- **描述**:所有列表类接口仅需 JWT 认证即可访问,无产品成员身份校验。已登录的任何用户可查看任意产品的角色列表、权限列表、成员列表。结合 H-5(Login 不验证成员关系),用户只需注册并使用任意 productCode 登录,即可遍历所有产品的内部数据。
|
|
|
|
|
-- **建议**:对需要 `productCode` 参数的列表接口,校验调用者是否为该产品的成员或超管。
|
|
|
|
|
|
|
+- **描述**:`createProductLogic.go` 创建产品后将 `AppSecret` 明文返回给前端,且 AppSecret 在数据库中也以明文存储。
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ return &types.CreateProductResp{
|
|
|
|
|
+ AppSecret: appSecret, // 明文返回
|
|
|
|
|
+ AdminPassword: adminPassword, // 管理员初始密码明文返回
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:AppSecret 相当于产品的 API 密钥,一旦通过网络被截获或日志记录,将可被用于调用 SyncPerms 等无需登录的接口。AdminPassword 同理。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:这是一次性展示场景(创建后只展示一次),风险可控。建议:
|
|
|
|
|
+ 1. 确保传输层使用 HTTPS
|
|
|
|
|
+ 2. 确认日志中不会记录响应体(当前 `response.go` 的错误处理不会记录成功响应体,但需确认 go-zero 框架层面的 access log 配置)
|
|
|
|
|
+ 3. AppSecret 数据库存储可考虑改为哈希存储,验证时对比哈希值
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### L-2: UserDetail/RoleDetail 未校验调用者是否有权查看
|
|
|
|
|
|
|
+### L-3:UserDetailsLoader.Load 中 singleflight 的 panic 风险
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`userDetailsLoader.go` 第 106-118 行:
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/user/userDetailLogic.go`、`internal/logic/role/roleDetailLogic.go`
|
|
|
|
|
-- **描述**:任何已认证用户可通过 ID 查看任意用户或角色的详情,包括用户的邮箱、手机号等个人信息。
|
|
|
|
|
-- **建议**:根据业务需求,考虑限制用户仅能查看本产品范围内的数据,或对敏感字段脱敏。
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ v, _, _ := l.sf.Do(key, func() (interface{}, error) {
|
|
|
|
|
+ ud, ok := l.loadFromDB(ctx, userId, productCode)
|
|
|
|
|
+ if ok {
|
|
|
|
|
+ // ... 缓存
|
|
|
|
|
+ }
|
|
|
|
|
+ return ud, nil
|
|
|
|
|
+ })
|
|
|
|
|
+ return v.(*UserDetails) // 如果 loadFromDB 的 ud 为 nil,此处会 panic
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+ `loadFromDB` 在 `loadUser` 失败时返回 `(ud, false)`,此时 `ud` 不是 nil(是一个初始化过的 `&UserDetails{}`),所以实际上不会 panic。但如果未来重构时 `loadFromDB` 的返回值逻辑变化,此处的类型断言缺乏安全检查。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:使用安全类型断言:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ ud, ok := v.(*UserDetails)
|
|
|
|
|
+ if !ok || ud == nil {
|
|
|
|
|
+ return &UserDetails{UserId: userId, ProductCode: productCode}
|
|
|
|
|
+ }
|
|
|
|
|
+ return ud
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### L-3: 部门删除缺少事务保护
|
|
|
|
|
|
|
+### L-4:BindRolesLogic 绑定角色未检查目标用户是否为产品成员
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`bindRolesLogic.go` 在为用户绑定角色时,只检查用户是否存在和角色是否属于当前产品,但**没有检查目标用户是否是当前产品的成员**。
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/dept/deleteDeptLogic.go:33-47`
|
|
|
|
|
-- **描述**:删除部门前先查子部门和关联用户,确认为空后再删除。但在检查和删除之间(虽然是极短窗口),理论上可能有新数据插入。在实际业务中,部门操作频率极低且限超管,此风险可接受。
|
|
|
|
|
-- **建议**:如追求严格一致性,可将检查与删除放入同一事务中,并在 SQL 层面做最终校验。
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ if _, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.UserId); err != nil {
|
|
|
|
|
+ return response.ErrNotFound("用户不存在")
|
|
|
|
|
+ }
|
|
|
|
|
+ // 缺少:检查用户是否为当前产品成员
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:可以为非产品成员的用户绑定角色,这些角色在 `loadRoles` 时会被加载但因用户不是成员所以 `loadMembership` 不会设置 MemberType,权限不会实际生效。但数据库中会存在孤立的无效关联数据。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:在绑定前校验目标用户是否为当前产品成员。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### L-4: DeptType 枚举校验不充分
|
|
|
|
|
|
|
+### L-5:SetUserPerms 同理未校验产品成员关系
|
|
|
|
|
|
|
|
-- **文件**:`internal/logic/dept/createDeptLogic.go:50-53`
|
|
|
|
|
-- **描述**:创建部门时,若 `req.DeptType` 为非空但非法值(如 `"INVALID"`),代码会直接使用该值(不经过枚举校验)。仅 `updateDeptLogic.go` 做了枚举校验。
|
|
|
|
|
-- **建议**:创建时也增加枚举校验:
|
|
|
|
|
|
|
+- **描述**:与 L-4 类似,`setUserPermsLogic.go` 在设置用户权限时没有检查目标用户是否为当前产品的成员,可以为非成员用户设置权限(虽然实际不会生效)。
|
|
|
|
|
|
|
|
-```go
|
|
|
|
|
-if deptType != consts.DeptTypeNormal && deptType != consts.DeptTypeDev {
|
|
|
|
|
- return nil, response.ErrBadRequest("无效的部门类型")
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+- **修复方案**:在设置前校验目标用户是否为当前产品成员。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### L-5: cleanByPattern 使用 SCAN 在 Redis Cluster 下可能遗漏 key
|
|
|
|
|
|
|
+### L-6:删除部门时忽略了 FindIdsByDeptId 的错误
|
|
|
|
|
+
|
|
|
|
|
+- **描述**:`deleteDeptLogic.go` 第 39 行使用 `_` 忽略了查询错误:
|
|
|
|
|
+
|
|
|
|
|
+ ```go
|
|
|
|
|
+ userIds, _ := l.svcCtx.SysUserModel.FindIdsByDeptId(l.ctx, req.Id)
|
|
|
|
|
+ if len(userIds) > 0 {
|
|
|
|
|
+ return response.ErrBadRequest("该部门下仍有关联用户,无法删除")
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+ 如果数据库查询出错(比如连接异常),`userIds` 为 nil,`len(userIds)` 为 0,会误判为"无关联用户",导致在仍有用户关联时删除部门。
|
|
|
|
|
+
|
|
|
|
|
+- **影响**:在数据库异常时可能导致误删有用户关联的部门,造成这些用户的 `deptId` 指向一个已不存在的部门。
|
|
|
|
|
+
|
|
|
|
|
+- **修复方案**:处理错误并在查询失败时阻止删除:
|
|
|
|
|
|
|
|
-- **文件**:`internal/loaders/userDetailsLoader.go:153-171`
|
|
|
|
|
-- **描述**:`CleanByProduct` 和 `Clean` 方法通过 Redis `SCAN` + pattern 匹配来批量删除缓存。在 Redis Cluster 模式下,`SCAN` 只扫描当前连接的节点,可能遗漏其他节点上的 key。当前配置为单节点 (`Type: node`),暂不受影响,但若未来迁移到 Cluster 需要注意。
|
|
|
|
|
-- **建议**:预留 Cluster 兼容方案,或在 key 设计中使用 hash tag 确保同一用户的 key 落在同一 slot。
|
|
|
|
|
|
|
+ ```go
|
|
|
|
|
+ userIds, err := l.svcCtx.SysUserModel.FindIdsByDeptId(l.ctx, req.Id)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 📋 审计总结
|
|
## 📋 审计总结
|
|
|
|
|
|
|
|
-| 级别 | 编号 | 问题 | 影响面 |
|
|
|
|
|
-|------|------|------|--------|
|
|
|
|
|
-| 🔴 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
|
|
|
|
|
|
|
+| 级别 | 编号 | 问题摘要 | 影响面 |
|
|
|
|
|
+|------|------|----------|--------|
|
|
|
|
|
+| 🔴 High | H-1 | loadPerms 跨产品权限泄漏 | 数据安全 |
|
|
|
|
|
+| 🔴 High | H-2 | UpdateUser 可绕过超管状态保护 | 权限控制 |
|
|
|
|
|
+| 🔴 High | H-3 | 禁用成员仍可登录 | 访问控制 |
|
|
|
|
|
+| 🔴 High | H-4 | gRPC VerifyToken 不检查实时状态 | 访问控制 |
|
|
|
|
|
+| 🔴 High | H-5 | gRPC Login 无速率限制 | 安全防护 |
|
|
|
|
|
+| 🟡 Medium | M-1 | Rate Limiter IP 可伪造 | 安全防护 |
|
|
|
|
|
+| 🟡 Medium | M-2 | RefreshToken 端点无速率限制 | 安全防护 |
|
|
|
|
|
+| 🟡 Medium | M-3 | 部门禁用不影响下属用户权限 | 逻辑一致性 |
|
|
|
|
|
+| 🟡 Medium | M-4 | Redis SCAN 不兼容 Cluster | 可部署性 |
|
|
|
|
|
+| 🟡 Medium | M-5 | HTTP/gRPC 登录逻辑重复 | 可维护性 |
|
|
|
|
|
+| 🟢 Low | L-1 | SyncPerms 无速率限制 | 安全防护 |
|
|
|
|
|
+| 🟢 Low | L-2 | CreateProduct 暴露 AppSecret | 信息泄漏 |
|
|
|
|
|
+| 🟢 Low | L-3 | singleflight 类型断言无安全检查 | 健壮性 |
|
|
|
|
|
+| 🟢 Low | L-4 | BindRoles 未校验产品成员关系 | 数据完整性 |
|
|
|
|
|
+| 🟢 Low | L-5 | SetUserPerms 未校验产品成员关系 | 数据完整性 |
|
|
|
|
|
+| 🟢 Low | L-6 | 删除部门时忽略查询错误 | 数据完整性 |
|
|
|
|
|
+
|
|
|
|
|
+**建议优先级**:H-1 > H-3 > H-2 > H-4 > H-5 > M-3 > M-1 > 其余
|
|
|
|
|
+
|
|
|
|
|
+H-1(跨产品权限泄漏)是最高优先级,因为它会导致静默的权限污染且难以被人工发现。H-3(禁用成员仍可登录)次之,因为管理员执行禁用操作后会产生"已生效"的错觉。
|