|
@@ -1,496 +1,342 @@
|
|
|
# 权限管理系统代码审计报告
|
|
# 权限管理系统代码审计报告
|
|
|
|
|
|
|
|
-> **审计范围**:`perms-system-server` 项目全部生产代码(排除 `*_test.go`)
|
|
|
|
|
-> **审计时间**:2026-04-16
|
|
|
|
|
-> **审计维度**:逻辑一致性、并发与竞态、资源管理、数据完整性、安全漏洞、边界崩溃
|
|
|
|
|
|
|
+> 审计时间:2026-04-17
|
|
|
|
|
+> 审计范围:`/internal` 下所有非测试 Go 源文件(含 model、logic、handler、middleware、loaders、server 等)
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 🚩 核心逻辑漏洞 (High Risk)
|
|
## 🚩 核心逻辑漏洞 (High Risk)
|
|
|
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### H-01:UpdateUser 允许普通用户修改自身 DeptId — 权限提升漏洞
|
|
|
|
|
|
|
+### H-1: BindRoles 全量删除导致跨产品角色绑定丢失
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/user/updateUserLogic.go:31-73`
|
|
|
|
|
-- **描述**:`UpdateUser` 的权限检查仅为"非超管只能改自己",但请求体中包含 `DeptId` 和 `Status` 字段。普通用户可以通过修改自己的 `DeptId` 将自己挪到一个 `DEV`(研发)类型部门下。根据 `loadPerms` 的逻辑(`userDetailsLoader.go:298`),研发部门成员自动获得当前产品的**全部权限**。
|
|
|
|
|
-- **影响**:任意已登录用户可将自己提升为拥有全量权限的用户,完全绕过 RBAC 权限体系。
|
|
|
|
|
-- **修复方案**:
|
|
|
|
|
- 1. 将 `DeptId` 和 `Status` 的修改权限从"自我编辑"中拆离,仅限超管或产品管理员操作;
|
|
|
|
|
- 2. 或者在 `UpdateUser` 中检查当 `callerId == req.Id` 时,禁止修改 `DeptId` 和 `Status` 字段:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/bindRolesLogic.go:62`
|
|
|
|
|
+- **描述**:为用户绑定角色时,事务中调用 `DeleteByUserIdTx` 删除了该用户在**所有产品**下的角色关联,随后仅重新插入当前产品的角色。`DeleteByUserIdTx` 的 SQL 为 `DELETE FROM sys_user_role WHERE userId = ?`,不带产品过滤。
|
|
|
|
|
+- **影响**:产品 A 的管理员为某用户绑定角色时,该用户在产品 B、C 等其他产品中已有的角色绑定会被一并清除,导致跨产品数据丢失,权限异常。这是一个**静默数据破坏**——操作者和被操作者均不会收到任何告警。
|
|
|
|
|
+- **修复方案**:将 `DeleteByUserIdTx` 替换为已有的 `DeleteByUserIdForProductTx`,仅删除当前产品下的角色关联:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-if callerId == req.Id {
|
|
|
|
|
- if req.DeptId != nil || req.Status != 0 {
|
|
|
|
|
- return response.ErrForbidden("不允许修改自己的部门和状态")
|
|
|
|
|
- }
|
|
|
|
|
-} else {
|
|
|
|
|
- if err := authHelper.CheckManageAccess(l.ctx, l.svcCtx, req.Id, productCode); err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// bindRolesLogic.go 事务内
|
|
|
|
|
+if err := l.svcCtx.SysUserRoleModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil {
|
|
|
|
|
+ return err
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### H-02:RefreshToken 允许跨产品切换 — 越权访问
|
|
|
|
|
|
|
+### H-2: SetUserPerms 全量删除导致跨产品用户直授权丢失
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/pub/refreshTokenLogic.go:42-45` 及 `internal/server/permserver.go:157-159`
|
|
|
|
|
-- **描述**:`RefreshToken` 接口允许请求参数中传入 `ProductCode` 来覆盖 refresh token 中原有的 `ProductCode`。这意味着用户只需获得任意一个产品的 refresh token,就能生成**另一个产品**的 access token,前提是该用户在目标产品中有成员记录。
|
|
|
|
|
-- **影响**:跨产品越权。用户用产品 A 的 refresh token 获取产品 B 的 access token(携带产品 B 的权限),即使产品 A 已被禁用。
|
|
|
|
|
-- **修复方案**:禁止 refresh token 更换产品上下文,或者在切换时进行严格的产品成员资格验证:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/setUserPermsLogic.go:69`
|
|
|
|
|
+- **描述**:与 H-1 同理,`DeleteByUserIdTx` 的 SQL 为 `DELETE FROM sys_user_perm WHERE userId = ?`,会删除用户在所有产品下的直授权记录,但随后仅重新插入当前产品的权限。
|
|
|
|
|
+- **影响**:产品 A 管理员设置用户权限时,用户在产品 B 的 ALLOW/DENY 直授权被清除。
|
|
|
|
|
+- **修复方案**:替换为 `DeleteByUserIdForProductTx`:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-if productCode != "" && productCode != claims.ProductCode {
|
|
|
|
|
- // 方案1:直接拒绝
|
|
|
|
|
- return nil, response.ErrBadRequest("不允许切换产品")
|
|
|
|
|
- // 方案2:验证目标产品成员资格
|
|
|
|
|
- // _, err := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(l.ctx, productCode, claims.UserId)
|
|
|
|
|
- // if err != nil {
|
|
|
|
|
- // return nil, response.ErrForbidden("您不是该产品成员")
|
|
|
|
|
- // }
|
|
|
|
|
|
|
+// setUserPermsLogic.go 事务内
|
|
|
|
|
+if err := l.svcCtx.SysUserPermModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil {
|
|
|
|
|
+ return err
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### H-03:BindRoles 不校验角色归属 — 跨产品角色绑定
|
|
|
|
|
|
|
+### H-3: RequireProductAdmin 未校验目标产品归属,存在跨产品越权
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/user/bindRolesLogic.go:42-59`
|
|
|
|
|
-- **描述**:`BindRoles` 接口直接删除用户所有角色绑定后,批量插入传入的 `RoleIds`。全过程**不校验**:
|
|
|
|
|
- 1. RoleId 是否存在;
|
|
|
|
|
- 2. RoleId 对应的角色是否属于当前操作者的产品上下文;
|
|
|
|
|
- 3. RoleId 对应的角色是否已启用。
|
|
|
|
|
-- **影响**:管理员可将其他产品的角色绑定到用户身上,可能导致权限计算逻辑混乱。外部传入非法 RoleId 不会报错,静默写入脏数据。
|
|
|
|
|
-- **修复方案**:
|
|
|
|
|
|
|
+- **文件**:`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
|
|
```go
|
|
|
-if len(req.RoleIds) > 0 {
|
|
|
|
|
- roles, err := l.svcCtx.SysRoleModel.FindByIds(l.ctx, req.RoleIds)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
|
|
+func RequireProductAdminFor(ctx context.Context, targetProductCode string) error {
|
|
|
|
|
+ caller := middleware.GetUserDetails(ctx)
|
|
|
|
|
+ if caller == nil {
|
|
|
|
|
+ return response.ErrUnauthorized("未登录")
|
|
|
}
|
|
}
|
|
|
- if len(roles) != len(req.RoleIds) {
|
|
|
|
|
- return response.ErrBadRequest("包含无效的角色ID")
|
|
|
|
|
|
|
+ if caller.IsSuperAdmin {
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
- for _, r := range roles {
|
|
|
|
|
- if r.ProductCode != productCode {
|
|
|
|
|
- return response.ErrBadRequest("不能绑定其他产品的角色")
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if caller.MemberType == consts.MemberTypeAdmin && caller.ProductCode == targetProductCode {
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
|
|
+ return response.ErrForbidden("仅超级管理员或该产品的管理员可执行此操作")
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### H-04:SetUserPerms 不校验权限 ID 和 Effect — 脏数据写入
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/user/setUserPermsLogic.go:42-61`
|
|
|
|
|
-- **描述**:`SetUserPerms` 接收 `PermId` 和 `Effect` 字段,但不校验:
|
|
|
|
|
- 1. `PermId` 是否存在或是否属于当前产品;
|
|
|
|
|
- 2. `Effect` 是否为合法值(`ALLOW` / `DENY`)。
|
|
|
|
|
- 虽然数据库 `effect` 列为 `enum('ALLOW','DENY')`,非法值会被 MySQL 拒绝并返回错误,但这依赖数据库约束而非应用层防御。
|
|
|
|
|
-- **影响**:写入无效 PermId 不报错;如果数据库 SQL Mode 不严格(如未启用 `STRICT_TRANS_TABLES`),非法 Effect 值可能被默认为空字符串静默写入。
|
|
|
|
|
-- **修复方案**:在业务层增加校验:
|
|
|
|
|
-
|
|
|
|
|
-```go
|
|
|
|
|
-for _, p := range req.Perms {
|
|
|
|
|
- if p.Effect != consts.PermEffectAllow && p.Effect != consts.PermEffectDeny {
|
|
|
|
|
- return response.ErrBadRequest("effect 值无效,仅支持 ALLOW 和 DENY")
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+然后在 `CreateRole`、`BindRolePerms`、`DeleteRole`、`UpdateRole` 中使用目标产品的 productCode 调用此函数。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### H-05:SyncPerms 三步操作无事务保护 — 数据中间态
|
|
|
|
|
|
|
+### H-4: BindRolePerms 不验证权限项归属于角色所在产品
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/pub/syncPermsLogic.go:80-93` 及 `internal/server/permserver.go:79-93`
|
|
|
|
|
-- **描述**:`SyncPerms` 依次执行 `BatchInsert` → `BatchUpdate` → `DisableNotInCodes` 三个独立的数据库操作,没有包裹在同一个事务中。如果 `BatchInsert` 成功但 `BatchUpdate` 或 `DisableNotInCodes` 失败,权限数据将处于不一致状态。
|
|
|
|
|
-- **影响**:
|
|
|
|
|
- - 部分权限被插入但旧权限未被更新/禁用,导致权限表"半更新";
|
|
|
|
|
- - 在高并发场景下,两个 SyncPerms 请求同时执行可能互相覆盖。
|
|
|
|
|
-- **修复方案**:将三步操作包裹在事务中:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/role/bindRolePermsLogic.go:31-65`
|
|
|
|
|
+- **描述**:为角色绑定权限时,仅验证角色存在,未校验 `req.PermIds` 中的权限是否属于该角色所在的产品。对比 `setUserPermsLogic.go` 中有做此验证(检查 `p.ProductCode != productCode`)。
|
|
|
|
|
+- **影响**:攻击者可将产品 B 的权限 ID 绑定到产品 A 的角色上。虽然最终 `loadPerms` 在计算用户权限时会按 productCode 过滤,不会直接导致越权执行,但会污染 `sys_role_perm` 表数据,导致角色详情返回不属于本产品的 permId,给前端和管理造成混乱。
|
|
|
|
|
+- **修复方案**:在事务之前增加权限归属校验:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-err = l.svcCtx.SysPermModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
|
|
|
|
|
- if len(toInsert) > 0 {
|
|
|
|
|
- if err := l.svcCtx.SysPermModel.BatchInsertWithTx(ctx, session, toInsert); err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
|
|
+if len(req.PermIds) > 0 {
|
|
|
|
|
+ perms, err := l.svcCtx.SysPermModel.FindByIds(l.ctx, req.PermIds)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
}
|
|
}
|
|
|
- if len(toUpdate) > 0 {
|
|
|
|
|
- if err := l.svcCtx.SysPermModel.BatchUpdateWithTx(ctx, session, toUpdate); 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("不能绑定其他产品的权限")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- // DisableNotInCodes 也需要提供 Tx 版本
|
|
|
|
|
- return nil
|
|
|
|
|
-})
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### H-06:SyncPerms AppSecret 明文比较 — 时序攻击风险
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/pub/syncPermsLogic.go:35` 及 `internal/server/permserver.go:34`
|
|
|
|
|
-- **描述**:`product.AppSecret != req.AppSecret` 使用普通字符串比较运算符,而不是 `crypto/subtle.ConstantTimeCompare`。Go 的 `!=` 运算在发现第一个不同字节时即返回,攻击者可通过测量响应时间逐字节暴力破解 AppSecret。
|
|
|
|
|
-- **影响**:具备网络时序测量能力的攻击者可逐步推断出完整的 AppSecret。
|
|
|
|
|
-- **修复方案**:
|
|
|
|
|
-
|
|
|
|
|
-```go
|
|
|
|
|
-import "crypto/subtle"
|
|
|
|
|
-
|
|
|
|
|
-if subtle.ConstantTimeCompare([]byte(product.AppSecret), []byte(req.AppSecret)) != 1 {
|
|
|
|
|
- return nil, response.ErrUnauthorized("appSecret验证失败")
|
|
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### H-07:DeleteDept 不检查关联用户 — 孤儿数据
|
|
|
|
|
|
|
+### H-5: Login/gRPC Login 不验证产品有效性和用户成员关系
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/dept/deleteDeptLogic.go:33-42`
|
|
|
|
|
-- **描述**:`DeleteDept` 仅检查是否有子部门,但**不检查**该部门下是否有关联用户(`sys_user.deptId`)。删除部门后,这些用户的 `deptId` 指向不存在的部门记录。
|
|
|
|
|
|
|
+- **文件**:`internal/logic/pub/loginLogic.go:32-90`、`internal/server/permserver.go:112-159`
|
|
|
|
|
+- **描述**:HTTP 和 gRPC 的 Login 接口均未验证:
|
|
|
|
|
+ 1. `productCode` 对应的产品是否存在且状态为启用
|
|
|
|
|
+ 2. 登录用户是否是该产品的成员
|
|
|
- **影响**:
|
|
- **影响**:
|
|
|
- - `UserDetailsLoader.loadDept` 会静默失败,`DeptPath` 为空字符串;
|
|
|
|
|
- - `checkDeptHierarchy` 中 `strings.HasPrefix(targetDept.Path, caller.DeptPath)` 永远返回 true(因为所有字符串都以 "" 为前缀),导致部门隔离机制失效。
|
|
|
|
|
-- **修复方案**:
|
|
|
|
|
|
|
+ - 任何用户可用任意 productCode(甚至不存在的)获取有效的 JWT access token
|
|
|
|
|
+ - 用户可为未加入的产品获取 token,虽然 perms 为空,但仍可访问仅需 JWT 认证的列表类接口(userList、roleList、permList、memberList 等),获得不应可见的数据
|
|
|
|
|
+- **修复方案**:在密码验证通过后、生成 token 前,增加产品和成员校验:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-userIds, _ := l.svcCtx.SysUserModel.FindIdsByDeptId(l.ctx, req.Id)
|
|
|
|
|
-if len(userIds) > 0 {
|
|
|
|
|
- return response.ErrBadRequest("该部门下仍有关联用户,无法删除")
|
|
|
|
|
|
|
+product, err := l.svcCtx.SysProductModel.FindOneByCode(l.ctx, req.ProductCode)
|
|
|
|
|
+if err != nil {
|
|
|
|
|
+ return nil, response.ErrBadRequest("产品不存在")
|
|
|
}
|
|
}
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### H-08:DeptPath 为空时部门隔离完全失效
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/auth/access.go:122`
|
|
|
|
|
-- **描述**:`checkDeptHierarchy` 使用 `strings.HasPrefix(targetDept.Path, caller.DeptPath)` 判断部门归属。如果 `caller.DeptPath` 为空字符串(例如用户部门被删除、或 loadDept 失败),`HasPrefix` 始终返回 `true`,使得任何人都能"管理"任何部门的用户。
|
|
|
|
|
-- **影响**:部门隔离机制被绕过。只要操作者的 DeptPath 加载失败(部门被删、Redis 缓存错误等),就获得跨部门管理权限。
|
|
|
|
|
-- **修复方案**:
|
|
|
|
|
-
|
|
|
|
|
-```go
|
|
|
|
|
-if caller.DeptPath == "" {
|
|
|
|
|
- return response.ErrForbidden("您的部门信息异常,无法执行此操作")
|
|
|
|
|
|
|
+if product.Status != consts.StatusEnabled {
|
|
|
|
|
+ return nil, response.ErrForbidden("该产品已被禁用")
|
|
|
}
|
|
}
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### H-09:配置文件包含明文密码和密钥
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`etc/perm-api-prod.yaml`(及其他环境配置文件)
|
|
|
|
|
-- **描述**:生产环境配置文件中以明文存储了 MySQL 密码、Redis 密码、JWT Secret、ManagementKey 等敏感信息,且这些文件被提交到 Git 仓库。
|
|
|
|
|
-- **影响**:任何可访问仓库的人(包括已离职员工、合作方、泄露的 Git 历史)都能获取全部生产凭据。
|
|
|
|
|
-- **修复方案**:
|
|
|
|
|
- 1. 将敏感信息移至环境变量或密钥管理服务(如 Vault、AWS Secrets Manager);
|
|
|
|
|
- 2. 将 `etc/*.yaml` 加入 `.gitignore`,从 Git 历史中清除已提交的密码;
|
|
|
|
|
- 3. **立即轮换**已泄露的所有密码和密钥。
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
-### H-10:CreateUser 缺少密码强度校验
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/user/createUserLogic.go:34-76`
|
|
|
|
|
-- **描述**:`CreateUser` 不检查密码长度和复杂度,但 `ChangePassword` 有 6-72 字符的限制。创建用户时可以设置 1 个字符甚至空字符串的密码。
|
|
|
|
|
-- **影响**:可创建弱密码用户,容易被暴力破解。
|
|
|
|
|
-- **修复方案**:统一密码校验逻辑:
|
|
|
|
|
-
|
|
|
|
|
-```go
|
|
|
|
|
-if len(req.Password) < 6 {
|
|
|
|
|
- return nil, response.ErrBadRequest("密码长度不能少于6个字符")
|
|
|
|
|
-}
|
|
|
|
|
-if len(req.Password) > 72 {
|
|
|
|
|
- return nil, response.ErrBadRequest("密码长度不能超过72个字符")
|
|
|
|
|
|
|
+_, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(l.ctx, req.ProductCode, u.Id)
|
|
|
|
|
+if memberErr != nil {
|
|
|
|
|
+ return nil, response.ErrForbidden("您不是该产品的成员")
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### H-11:UserList / UserDetail / RoleList 等查询接口缺少权限过滤
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:
|
|
|
|
|
- - `internal/logic/user/userListLogic.go` — 无权限检查,查询全表
|
|
|
|
|
- - `internal/logic/user/userDetailLogic.go` — 无权限检查,可查任意用户
|
|
|
|
|
- - `internal/logic/role/roleListLogic.go` — 无权限检查
|
|
|
|
|
- - `internal/logic/role/roleDetailLogic.go` — 无权限检查,可查任意产品角色
|
|
|
|
|
- - `internal/logic/product/productListLogic.go` — 无权限检查,AppKey 泄露
|
|
|
|
|
- - `internal/logic/product/productDetailLogic.go` — 无权限检查,AppKey 泄露
|
|
|
|
|
-- **描述**:所有列表/详情查询接口虽然经过 JWT 认证,但不做任何业务权限过滤。任何已登录用户可以:
|
|
|
|
|
- - 查看系统中所有用户的信息(含邮箱、手机号)
|
|
|
|
|
- - 查看所有产品的 AppKey
|
|
|
|
|
- - 查看任何产品的角色和权限详情
|
|
|
|
|
-- **影响**:敏感信息大面积泄露;违反最小权限原则。
|
|
|
|
|
|
|
+### 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
|
|
|
- **修复方案**:
|
|
- **修复方案**:
|
|
|
- - `UserList` 应按操作者的产品/部门作用域进行过滤;
|
|
|
|
|
- - `ProductList/ProductDetail` 的 `AppKey` 字段仅对超管可见;
|
|
|
|
|
- - `RoleDetail` 应校验操作者是否有权查看该产品的角色。
|
|
|
|
|
|
|
+ 1. 将 `etc/*.yaml` 加入 `.gitignore`,从 Git 历史中清除(`git filter-branch` 或 BFG)
|
|
|
|
|
+ 2. 使用环境变量或密钥管理服务(如 Vault、AWS Secrets Manager)注入敏感配置
|
|
|
|
|
+ 3. 立即轮换所有已泄露的密码和 secret
|
|
|
|
|
+ 4. 为 dev/test/prod 使用不同的凭据
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## ⚠️ 健壮性与性能建议 (Medium/Low)
|
|
## ⚠️ 健壮性与性能建议 (Medium/Low)
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+### M-1: CreateRole/UpdateRole 未校验 PermsLevel 范围
|
|
|
|
|
|
|
|
-### M-01:ChangePassword / UpdateUserStatus 存在 Read-Modify-Write 竞态
|
|
|
|
|
|
|
+- **文件**:`internal/logic/role/createRoleLogic.go:38`、`updateRoleLogic.go:42`
|
|
|
|
|
+- **描述**:`PermsLevel` 字段无范围校验,可设为 0、负数或任意大整数。`checkPermLevel` 中通过 `MinPermsLevel` 数值比较来判定管理权限(数值越小权限越高),若不限制范围可能导致权限比较逻辑出现意外行为。
|
|
|
|
|
+- **建议**:增加 PermsLevel 的合法范围校验(如 1-999),确保符合业务预期:
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/auth/changePasswordLogic.go:40-63` 及 `internal/logic/user/updateUserStatusLogic.go:41-58`
|
|
|
|
|
-- **描述**:先 `FindOne` 读取整条记录,修改某个字段后整条 `Update`。在高并发场景下,两个操作可能同时读取到相同的旧数据,后写入者覆盖先写入者的修改。
|
|
|
|
|
-- **风险等级**:Medium(密码修改并发场景较少,但状态修改可能并发)
|
|
|
|
|
-- **建议**:对关键字段使用针对性 UPDATE 语句,而非全字段覆盖:
|
|
|
|
|
-
|
|
|
|
|
-```sql
|
|
|
|
|
-UPDATE sys_user SET password = ?, mustChangePassword = ?, updateTime = ? WHERE id = ?
|
|
|
|
|
|
|
+```go
|
|
|
|
|
+if req.PermsLevel < 1 || req.PermsLevel > 999 {
|
|
|
|
|
+ return nil, response.ErrBadRequest("权限级别必须在 1-999 之间")
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-02:UserDetailsLoader 无缓存击穿防护
|
|
|
|
|
|
|
+### M-2: UpdateUser 不验证 DeptId 有效性
|
|
|
|
|
|
|
|
-- **位置**:`internal/loaders/userDetailsLoader.go:94-113`
|
|
|
|
|
-- **描述**:`Load` 方法未使用 `singleflight` 或分布式锁。当缓存过期瞬间,大量并发请求会同时回源数据库执行 6 次查询(loadUser + loadDept + loadProduct + loadMembership + loadRoles + loadPerms)。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
-- **建议**:使用 `golang.org/x/sync/singleflight` 对相同 key 的并发请求进行去重:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/updateUserLogic.go:70-72`
|
|
|
|
|
+- **描述**:超级管理员修改用户部门时,不检查 `DeptId` 是否对应一个存在且启用的部门。若设置了不存在的 DeptId,后续 `checkDeptHierarchy` 中查询 `SysDeptModel.FindOne(target.DeptId)` 会失败,返回「无权操作」的误导性错误。
|
|
|
|
|
+- **建议**:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-import "golang.org/x/sync/singleflight"
|
|
|
|
|
-
|
|
|
|
|
-type UserDetailsLoader struct {
|
|
|
|
|
- // ...
|
|
|
|
|
- sf singleflight.Group
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode string) *UserDetails {
|
|
|
|
|
- key := l.cacheKey(userId, productCode)
|
|
|
|
|
- if val, err := l.rds.GetCtx(ctx, key); err == nil && val != "" {
|
|
|
|
|
- var ud UserDetails
|
|
|
|
|
- if err := json.Unmarshal([]byte(val), &ud); err == nil {
|
|
|
|
|
- return &ud
|
|
|
|
|
- }
|
|
|
|
|
|
|
+if req.DeptId != nil && *req.DeptId > 0 {
|
|
|
|
|
+ if _, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, *req.DeptId); err != nil {
|
|
|
|
|
+ return response.ErrBadRequest("部门不存在")
|
|
|
}
|
|
}
|
|
|
- v, _, _ := l.sf.Do(key, func() (interface{}, error) {
|
|
|
|
|
- return l.loadFromDB(ctx, userId, productCode), nil
|
|
|
|
|
- })
|
|
|
|
|
- ud := v.(*UserDetails)
|
|
|
|
|
- // set cache ...
|
|
|
|
|
- return ud
|
|
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-03:cleanByPattern Lua 脚本在 Redis Cluster 下不兼容
|
|
|
|
|
|
|
+### M-3: UserDetailsLoader.loadFromDB 静默吞错误,可能缓存不完整数据
|
|
|
|
|
|
|
|
-- **位置**:`internal/loaders/userDetailsLoader.go:149-169`
|
|
|
|
|
-- **描述**:`cleanByPattern` 使用 `SCAN` + `DEL` 的 Lua 脚本传入 `[]string{}` 作为 KEYS(空切片),但在 Redis Cluster 中,所有脚本操作的 key 必须位于同一个 slot,且必须通过 KEYS 参数传入。使用 `ARGV` 传递模式+SCAN 在 Cluster 环境下会被拒绝。
|
|
|
|
|
-- **风险等级**:Medium(当前使用单节点 Redis,但未来扩展 Cluster 时会直接报错)
|
|
|
|
|
-- **建议**:改用 Go 侧循环 `SCAN` + Pipeline `DEL` 替代 Lua 脚本:
|
|
|
|
|
|
|
+- **文件**:`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
|
|
```go
|
|
|
-func (l *UserDetailsLoader) cleanByPattern(ctx context.Context, pattern string) {
|
|
|
|
|
- var cursor uint64
|
|
|
|
|
- for {
|
|
|
|
|
- keys, cur, err := l.rds.ScanCtx(ctx, cursor, pattern, 100)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- logx.WithContext(ctx).Errorf("scan keys failed: %v", err)
|
|
|
|
|
- return
|
|
|
|
|
|
|
+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)
|
|
|
}
|
|
}
|
|
|
- if len(keys) > 0 {
|
|
|
|
|
- if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
|
|
|
|
|
- logx.WithContext(ctx).Errorf("del keys failed: %v", err)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if cur == 0 {
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- cursor = cur
|
|
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+ // 加载失败时不缓存,让下次请求重试
|
|
|
|
|
+ return ud, nil
|
|
|
|
|
+})
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-04:gRPC Server 在 goroutine 中启动,失败不可感知
|
|
|
|
|
|
|
+### M-4: AdminLogin 不限制仅超管用户登录
|
|
|
|
|
|
|
|
-- **位置**:`perm.go:32-40`
|
|
|
|
|
-- **描述**:gRPC Server 通过 `go func()` 在后台 goroutine 中启动。如果 gRPC Server 启动失败(如端口冲突)或运行时 panic,主进程(HTTP Server)不会感知,继续以"残缺"状态运行。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
-- **建议**:使用 `errgroup` 或 channel 同步两个 server 的生命周期:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/pub/adminLoginLogic.go:33-91`
|
|
|
|
|
+- **描述**:`adminLogin` 接口仅验证 `managementKey` 后即允许任何用户登录管理后台,包括普通用户。虽然 `managementKey` 本身是一道屏障,但从设计意图来看管理后台应仅面向超管。
|
|
|
|
|
+- **建议**:如管理后台仅面向超管,可在密码验证后增加:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-errCh := make(chan error, 1)
|
|
|
|
|
-go func() {
|
|
|
|
|
- rpcServer := zrpc.MustNewServer(...)
|
|
|
|
|
- defer rpcServer.Stop()
|
|
|
|
|
- rpcServer.Start()
|
|
|
|
|
- errCh <- nil
|
|
|
|
|
-}()
|
|
|
|
|
-
|
|
|
|
|
-// 主 goroutine 也监听 errCh
|
|
|
|
|
|
|
+if u.IsSuperAdmin != consts.IsSuperAdminYes {
|
|
|
|
|
+ return nil, response.ErrForbidden("管理后台仅限超级管理员登录")
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-05:gen 代码修改包级全局变量 — 初始化竞态风险
|
|
|
|
|
|
|
+### M-5: CreateUser 用户名唯一性存在 TOCTOU 窗口
|
|
|
|
|
|
|
|
-- **位置**:所有 `*_gen.go` 文件中的 `newSys*Model` 函数(如 `sysPermModel_gen.go:70-71`、`sysUserModel_gen.go:77-78`)
|
|
|
|
|
-- **描述**:`newSysPermModel` 等函数直接修改包级别的 `cacheSysPermIdPrefix` 等全局变量。虽然当前 `NewModels` 在启动时仅调用一次,但这种模式不是并发安全的——如果将来在测试或多实例场景下并发初始化,会发生数据竞争。
|
|
|
|
|
-- **风险等级**:Low(当前安全,但代码气味不好)
|
|
|
|
|
-- **建议**:将 cache prefix 存储为实例字段而非包级变量(需修改代码生成模板)。
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/createUserLogic.go:53-56`
|
|
|
|
|
+- **描述**:先通过 `FindOneByUsername` 检查用户名是否存在,再执行 `Insert`。在两个操作之间,其他并发请求可能已经插入了同名用户。虽然代码在 Insert 失败时通过检查 `"1062"` / `"Duplicate entry"` 做了补偿处理,但这种双重检查模式产生了不必要的复杂度。
|
|
|
|
|
+- **建议**:可以去掉前置的 `FindOneByUsername` 查询,直接依赖数据库唯一索引约束 + `Duplicate entry` 错误判断即可,减少一次 DB 查询。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-06:MemberType 缺少应用层白名单校验
|
|
|
|
|
|
|
+### M-6: gRPC Login 可用于暴力枚举用户密码
|
|
|
|
|
|
|
|
-- **位置**:
|
|
|
|
|
- - `internal/logic/member/addMemberLogic.go:31` — `req.MemberType` 无校验
|
|
|
|
|
- - `internal/logic/member/updateMemberLogic.go:30` — `req.MemberType` 无校验
|
|
|
|
|
-- **描述**:`MemberType` 字段完全依赖数据库 `enum('DEVELOPER','ADMIN','MEMBER')` 约束来限制合法值,应用层不做白名单校验。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
|
|
+- **文件**:`internal/server/permserver.go:112-159`
|
|
|
|
|
+- **描述**:gRPC Login 接口无速率限制。在内部网络中,攻击者可对 gRPC 端口发起高速暴力破解(bcrypt 有计算成本,但大量并发仍可造成 CPU 压力和逐步猜测)。HTTP 侧通常有 Nginx/WAF 保护,但 gRPC 端口可能缺少此层防护。
|
|
|
- **建议**:
|
|
- **建议**:
|
|
|
|
|
+ - 在 gRPC server 添加速率限制 interceptor
|
|
|
|
|
+ - 或确保 gRPC 端口不对外暴露,仅内部服务间调用
|
|
|
|
|
|
|
|
-```go
|
|
|
|
|
-validTypes := map[string]bool{
|
|
|
|
|
- consts.MemberTypeAdmin: true,
|
|
|
|
|
- consts.MemberTypeDeveloper: true,
|
|
|
|
|
- consts.MemberTypeMember: true,
|
|
|
|
|
-}
|
|
|
|
|
-if !validTypes[req.MemberType] {
|
|
|
|
|
- return nil, response.ErrBadRequest("无效的成员类型")
|
|
|
|
|
-}
|
|
|
|
|
-```
|
|
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 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-07:generateRandomHex 忽略 crypto/rand.Read 错误
|
|
|
|
|
|
|
+### M-8: BindRoles 中角色 ID 去重缺失
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/product/createProductLogic.go:117-121`
|
|
|
|
|
-- **描述**:`rand.Read(b)` 的返回错误被忽略。虽然 `crypto/rand` 在主流系统上几乎不会失败,但在某些极端环境(如容器中 `/dev/urandom` 不可用)下可能返回错误,此时 `b` 包含零值或不完整的随机数据。
|
|
|
|
|
-- **风险等级**:Low
|
|
|
|
|
-- **建议**:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/bindRolesLogic.go:69-78`
|
|
|
|
|
+- **描述**:`req.RoleIds` 未做去重处理。若前端传入 `[1, 1, 1]`,会在 `sys_user_role` 表中插入三条相同的记录(除非表有唯一联合索引)。
|
|
|
|
|
+- **建议**:在插入前对 `req.RoleIds` 去重:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-func generateRandomHex(length int) (string, error) {
|
|
|
|
|
- b := make([]byte, length)
|
|
|
|
|
- if _, err := rand.Read(b); err != nil {
|
|
|
|
|
- return "", fmt.Errorf("generate random bytes failed: %w", err)
|
|
|
|
|
|
|
+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)
|
|
|
}
|
|
}
|
|
|
- return hex.EncodeToString(b)[:length], nil
|
|
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-08:DeptTree 全表加载无上限限制
|
|
|
|
|
|
|
+### M-9: SetUserPerms 中 PermId 去重缺失
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/dept/deptTreeLogic.go:27`
|
|
|
|
|
-- **描述**:`DeptTree` 使用 `FindAll` 一次性加载所有部门数据到内存中构建树。如果部门数量增长到数万级别,会导致内存占用激增和响应延迟。
|
|
|
|
|
-- **风险等级**:Low(当前数据量小)
|
|
|
|
|
-- **建议**:考虑添加分页或根据 `parentId` 按需加载子树。
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/setUserPermsLogic.go:49-66`
|
|
|
|
|
+- **描述**:同 M-8,`req.Perms` 中若存在重复的 PermId,会插入多条记录。此外,对同一 PermId 若传入了不同 Effect(一个 ALLOW 一个 DENY),语义冲突但代码不会拦截。
|
|
|
|
|
+- **建议**:对 PermId 去重,并检查同一 PermId 不能同时为 ALLOW 和 DENY。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-09:SyncPerms 缺乏去重校验
|
|
|
|
|
|
|
+### L-1: UserList/MemberList/RoleList 等列表接口缺少产品级权限控制
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/pub/syncPermsLogic.go:54-78`
|
|
|
|
|
-- **描述**:如果请求中的 `Perms` 数组包含重复的 `Code`,后出现的会覆盖前出现的在 `existingMap` 中的查询结果,但 `toInsert` 中可能包含重复条目,导致 `BatchInsert` 时触发唯一键冲突。
|
|
|
|
|
-- **风险等级**:Low
|
|
|
|
|
-- **建议**:在处理前对 `codes` 去重。
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/userListLogic.go`、`internal/logic/member/memberListLogic.go`、`internal/logic/role/roleListLogic.go` 等
|
|
|
|
|
+- **描述**:所有列表类接口仅需 JWT 认证即可访问,无产品成员身份校验。已登录的任何用户可查看任意产品的角色列表、权限列表、成员列表。结合 H-5(Login 不验证成员关系),用户只需注册并使用任意 productCode 登录,即可遍历所有产品的内部数据。
|
|
|
|
|
+- **建议**:对需要 `productCode` 参数的列表接口,校验调用者是否为该产品的成员或超管。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-10:UpdateDept 修改 deptType 后未级联刷新子部门用户缓存
|
|
|
|
|
|
|
+### L-2: UserDetail/RoleDetail 未校验调用者是否有权查看
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/dept/updateDeptLogic.go:43-58`
|
|
|
|
|
-- **描述**:`UpdateDept` 仅清除该部门直接关联用户的缓存。如果修改了 `deptType`(如 `DEV` → `NORMAL`),子部门的用户权限可能因为父部门类型变更而需要重新计算,但不会被刷新。
|
|
|
|
|
-- **风险等级**:Medium(依赖于部门类型是否影响子部门的权限逻辑)
|
|
|
|
|
-- **建议**:当 `deptType` 变更时,清除所有子部门用户的缓存。
|
|
|
|
|
|
|
+- **文件**:`internal/logic/user/userDetailLogic.go`、`internal/logic/role/roleDetailLogic.go`
|
|
|
|
|
+- **描述**:任何已认证用户可通过 ID 查看任意用户或角色的详情,包括用户的邮箱、手机号等个人信息。
|
|
|
|
|
+- **建议**:根据业务需求,考虑限制用户仅能查看本产品范围内的数据,或对敏感字段脱敏。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-11:Login/AdminLogin 无暴力破解防护
|
|
|
|
|
|
|
+### L-3: 部门删除缺少事务保护
|
|
|
|
|
|
|
|
-- **位置**:`internal/logic/pub/loginLogic.go`、`internal/logic/pub/adminLoginLogic.go`
|
|
|
|
|
-- **描述**:登录接口没有速率限制、验证码或账号锁定机制。攻击者可无限次尝试暴力破解密码。`AdminLogin` 的 `ManagementKey` 也是静态值,无速率限制保护。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
-- **建议**:
|
|
|
|
|
- 1. 接入 go-zero 的 `PeriodLimit` 或 Redis 滑动窗口限流;
|
|
|
|
|
- 2. 连续失败 N 次后临时锁定账号或要求验证码。
|
|
|
|
|
|
|
+- **文件**:`internal/logic/dept/deleteDeptLogic.go:33-47`
|
|
|
|
|
+- **描述**:删除部门前先查子部门和关联用户,确认为空后再删除。但在检查和删除之间(虽然是极短窗口),理论上可能有新数据插入。在实际业务中,部门操作频率极低且限超管,此风险可接受。
|
|
|
|
|
+- **建议**:如追求严格一致性,可将检查与删除放入同一事务中,并在 SQL 层面做最终校验。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### M-12:ProductList 和 ProductDetail 泄露 AppKey
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/product/productListLogic.go:37` 及 `internal/logic/product/productDetailLogic.go:33`
|
|
|
|
|
-- **描述**:`ProductItem` 响应体包含 `AppKey` 字段,任何已登录用户均可获取。`AppKey` 是产品的接入标识,泄露后配合时序攻击(H-06)有助于暴力破解 `AppSecret`。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
-- **建议**:`AppKey` 仅对超管可见,或在列表接口中排除该字段。
|
|
|
|
|
|
|
+### L-4: DeptType 枚举校验不充分
|
|
|
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### M-13:BindRoles 事务后的缓存清理仅删除单个产品维度
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/user/bindRolesLogic.go:64`
|
|
|
|
|
-- **描述**:`BindRoles` 先删除用户全部角色绑定(不限产品),再插入新角色。但缓存清理只 `Del` 了当前操作者的 `productCode` 维度。如果用户在多个产品中都有角色绑定,其他产品的缓存不会被清除,导致缓存与 DB 不一致。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
-- **建议**:使用 `Clean`(通配删除所有产品缓存)代替 `Del`:
|
|
|
|
|
|
|
+- **文件**:`internal/logic/dept/createDeptLogic.go:50-53`
|
|
|
|
|
+- **描述**:创建部门时,若 `req.DeptType` 为非空但非法值(如 `"INVALID"`),代码会直接使用该值(不经过枚举校验)。仅 `updateDeptLogic.go` 做了枚举校验。
|
|
|
|
|
+- **建议**:创建时也增加枚举校验:
|
|
|
|
|
|
|
|
```go
|
|
```go
|
|
|
-l.svcCtx.UserDetailsLoader.Clean(l.ctx, req.UserId)
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### M-14:CreateProduct 响应返回明文密码和 AppSecret
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/logic/product/createProductLogic.go:107-115`
|
|
|
|
|
-- **描述**:`CreateProductResp` 中包含 `AdminPassword`(明文)和 `AppSecret`。如果响应被日志框架记录或被中间件拦截,这些敏感信息会出现在日志中。
|
|
|
|
|
-- **风险等级**:Medium
|
|
|
|
|
-- **建议**:确保该接口的响应不被 access log 或审计日志完整记录;或者改为只生成一次性查看链接。
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### L-01:Response 统一返回 HTTP 200 — 不利于监控
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/response/response.go:44-50`
|
|
|
|
|
-- **描述**:所有业务错误(401、403、404 等)的 HTTP 状态码均返回 `200`,仅在 body 中的 `code` 字段区分。这导致 HTTP 监控(如 Nginx、ALB 的 5xx 告警)无法发现业务异常。
|
|
|
|
|
-- **风险等级**:Low
|
|
|
|
|
-- **建议**:视项目规范决定是否保持,但至少应确保 APM 层能抓取 body 中的 code 字段做告警。
|
|
|
|
|
-
|
|
|
|
|
----
|
|
|
|
|
-
|
|
|
|
|
-### L-02:FindListByDeptIds 的 append 可能修改原 slice
|
|
|
|
|
-
|
|
|
|
|
-- **位置**:`internal/model/user/sysUserModel.go:69`
|
|
|
|
|
-- **描述**:`pageArgs := append(args, ...)` 如果 `args` 底层数组容量足够,会修改 `args` 的底层数据。虽然 `args` 在此处之后不再使用,但这是一个潜在的陷阱。
|
|
|
|
|
-- **风险等级**:Low
|
|
|
|
|
-- **建议**:使用显式拷贝:
|
|
|
|
|
-
|
|
|
|
|
-```go
|
|
|
|
|
-pageArgs := make([]interface{}, len(args), len(args)+2)
|
|
|
|
|
-copy(pageArgs, args)
|
|
|
|
|
-pageArgs = append(pageArgs, (page-1)*pageSize, pageSize)
|
|
|
|
|
|
|
+if deptType != consts.DeptTypeNormal && deptType != consts.DeptTypeDev {
|
|
|
|
|
+ return nil, response.ErrBadRequest("无效的部门类型")
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-### L-03:权限计算逻辑在两处重复(loadPerms 与 GetUserPerms)
|
|
|
|
|
|
|
+### L-5: cleanByPattern 使用 SCAN 在 Redis Cluster 下可能遗漏 key
|
|
|
|
|
|
|
|
-- **位置**:`internal/loaders/userDetailsLoader.go:289-357` 与 `internal/logic/auth/perms.go:11-115`
|
|
|
|
|
-- **描述**:权限计算逻辑(角色权限 + 用户 allow - 用户 deny)在 `UserDetailsLoader.loadPerms` 和 `GetUserPerms` 中各实现了一遍。逻辑高度相似但不完全相同(如 `loadPerms` 多了 `DeptType == DEV` 的判断),容易在后续维护中出现不一致。
|
|
|
|
|
-- **风险等级**:Low
|
|
|
|
|
-- **建议**:将权限计算逻辑抽取为单一函数,两处共用。
|
|
|
|
|
|
|
+- **文件**:`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 | 11 | 权限提升、跨产品越权、数据完整性缺陷、敏感信息泄露 |
|
|
|
|
|
-| ⚠️ Medium | 14 | 竞态条件、缓存不一致、缺少暴力破解防护、输入校验不足 |
|
|
|
|
|
-| 📝 Low | 3 | 代码重复、HTTP 状态码规范、slice append 陷阱 |
|
|
|
|
|
|
|
+| 级别 | 编号 | 问题 | 影响面 |
|
|
|
|
|
+|------|------|------|--------|
|
|
|
|
|
+| 🔴 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
|