|
|
@@ -1,8 +1,8 @@
|
|
|
-# 权限管理系统 (perms-system-server) 深度代码审计报告
|
|
|
+# 权限管理系统 - 深度代码审计报告
|
|
|
|
|
|
-> 审计时间:2026-04-17
|
|
|
-> 审计范围:全部非测试业务源代码(logic / model / middleware / loaders / server / handler)
|
|
|
-> 审计维度:逻辑一致性、并发与竞态、资源管理、数据完整性、安全漏洞、边界崩溃
|
|
|
+> 审计范围:`internal/` 下所有非测试源代码,包括 logic、model、middleware、handler、config、loader、server 层
|
|
|
+> 审计时间:2026-04-17
|
|
|
+> 排除范围:`*_test.go`、`*_mock_test.go`、`testutil/`、`cli/`、`pb/`(生成代码)
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -10,228 +10,170 @@
|
|
|
|
|
|
---
|
|
|
|
|
|
-### H-1:UserDetailsLoader.loadPerms 跨产品权限泄漏
|
|
|
-
|
|
|
-- **描述**:`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 中
|
|
|
-
|
|
|
- 更严重的是:产品 B 中的 DENY 规则也会泄漏,可能**错误阻止**产品 A 中本应拥有的权限。
|
|
|
+### H1. AdminLogin 未校验用户是否为超级管理员 —— 越权访问风险
|
|
|
|
|
|
+- **文件**:`internal/logic/pub/adminLoginLogic.go:33-91`
|
|
|
+- **描述**:`AdminLogin` 接口仅校验了 `ManagementKey` 和用户名密码,但**没有校验用户的 `isSuperAdmin` 字段**。任何普通用户只要知道 `ManagementKey`,就能通过管理后台登录接口获取一个 `productCode=""` 的 Token。
|
|
|
- **影响**:
|
|
|
- - 跨产品权限授予:用户在未授权的产品中获得额外权限
|
|
|
- - 跨产品权限拒绝:产品 B 的 DENY 规则错误地屏蔽产品 A 的合法权限
|
|
|
- - JWT Token 中包含其他产品的权限信息(信息泄漏)
|
|
|
-
|
|
|
+ - 拿到此 Token 后,用户通过 JWT 中间件校验,可以调用所有 `JwtAuth` 保护的接口。
|
|
|
+ - 虽然 `RequireSuperAdmin()` 会在创建产品、管理部门等操作中拦截,但 `UserDetail`、`UserList`、`RoleList`、`ProductList`、`DeptTree` 等查询接口**没有额外权限校验**,非超管用户可以通过此途径浏览所有系统数据。
|
|
|
+ - `ManagementKey` 一旦泄露(如被抓包、配置文件泄露),整个系统对该用户门户大开。
|
|
|
- **修复方案**:
|
|
|
|
|
|
- 方案一(推荐):在 `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
|
|
|
- }
|
|
|
- ```
|
|
|
+```go
|
|
|
+// adminLoginLogic.go — 在密码验证通过后增加超管校验
|
|
|
+if u.IsSuperAdmin != consts.IsSuperAdminYes {
|
|
|
+ return nil, response.ErrForbidden("仅超级管理员可通过管理后台登录")
|
|
|
+}
|
|
|
+```
|
|
|
|
|
|
---
|
|
|
|
|
|
-### H-2:UpdateUser 接口可绕过超管状态保护
|
|
|
+### H2. 限流中间件 IP 提取逻辑有两个严重缺陷
|
|
|
|
|
|
-- **描述**:`internal/logic/user/updateUserStatusLogic.go` 明确禁止修改超级管理员的状态:
|
|
|
-
|
|
|
- ```go
|
|
|
- if user.IsSuperAdmin == consts.IsSuperAdminYes {
|
|
|
- return response.ErrForbidden("不能修改超级管理员的状态")
|
|
|
- }
|
|
|
- ```
|
|
|
-
|
|
|
- 但 `internal/logic/user/updateUserLogic.go` 中也接受 `Status` 字段,对于非本人操作只要求调用者是超管,**没有检查目标用户是否也是超管**:
|
|
|
+- **文件**:`internal/middleware/ratelimitMiddleware.go:24-28`
|
|
|
+- **描述**:
|
|
|
+ 1. **`r.RemoteAddr` 包含端口号**:Go 的 `http.Request.RemoteAddr` 格式为 `IP:Port`(如 `192.168.1.1:54321`)。由于每个 TCP 连接的临时端口不同,限流 Key 变成了**每个连接独立计数**,同一个客户端 IP 几乎不可能触发限流。
|
|
|
+ 2. **未处理反向代理场景**:生产环境通常有 Nginx/Envoy 做反向代理,此时 `RemoteAddr` 是代理服务器的 IP,所有客户端共享同一个限流桶,导致少量正常请求就会触发全局限流。
|
|
|
+- **影响**:登录接口(`/auth/login`、`/auth/adminLogin`)的暴力破解防护**形同虚设**。攻击者可以不受限制地进行密码爆破。
|
|
|
+- **修复方案**:
|
|
|
|
|
|
- ```go
|
|
|
- } else {
|
|
|
- if !caller.IsSuperAdmin {
|
|
|
- return response.ErrForbidden("仅允许修改自己的信息或超管操作")
|
|
|
- }
|
|
|
- }
|
|
|
- // ... 后续直接设置 status,无超管保护
|
|
|
- if req.Status == consts.StatusEnabled || req.Status == consts.StatusDisabled {
|
|
|
- user.Status = req.Status
|
|
|
- }
|
|
|
- ```
|
|
|
+```go
|
|
|
+func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
|
|
+ return func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ ip := extractClientIP(r)
|
|
|
+ key := fmt.Sprintf("ip:%s", ip)
|
|
|
+ code, _ := m.limiter.Take(key)
|
|
|
+ if code == limit.OverQuota {
|
|
|
+ httpx.ErrorCtx(r.Context(), w, response.ErrTooManyRequests("请求过于频繁,请稍后再试"))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ next(w, r)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- 攻击路径:超管 A 通过 `POST /api/user/update` 传入 `{"id": <超管B的ID>, "status": 2}`,即可冻结超管 B,绕过 `updateUserStatus` 的保护逻辑。
|
|
|
+func extractClientIP(r *http.Request) string {
|
|
|
+ // 优先从反代标准头提取
|
|
|
+ if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
|
|
+ return ip
|
|
|
+ }
|
|
|
+ if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
|
+ // 取第一个 IP(最靠近客户端的)
|
|
|
+ if idx := strings.Index(forwarded, ","); idx != -1 {
|
|
|
+ return strings.TrimSpace(forwarded[:idx])
|
|
|
+ }
|
|
|
+ return strings.TrimSpace(forwarded)
|
|
|
+ }
|
|
|
+ // 兜底:去掉端口
|
|
|
+ host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
|
+ if err != nil {
|
|
|
+ return r.RemoteAddr
|
|
|
+ }
|
|
|
+ return host
|
|
|
+}
|
|
|
+```
|
|
|
|
|
|
-- **影响**:超级管理员之间可互相冻结账号,破坏系统管理根基。在最坏场景下,攻击者获取任一超管账号后可瘫痪所有其他超管。
|
|
|
+---
|
|
|
|
|
|
-- **修复方案**:在 `updateUserLogic.go` 中增加超管保护检查:
|
|
|
+### H3. 多个查询接口存在水平越权 —— 无跨产品/无权限校验
|
|
|
+
|
|
|
+- **文件**:
|
|
|
+ - `internal/logic/user/userDetailLogic.go` — 任意用户可查看任意用户详情
|
|
|
+ - `internal/logic/user/userListLogic.go` — 任意用户可列出全系统所有用户
|
|
|
+ - `internal/logic/role/roleListLogic.go` — 可传入任意 `productCode` 查看其他产品角色
|
|
|
+ - `internal/logic/role/roleDetailLogic.go` — 可查看任意角色详情(含所绑定的权限 ID 列表)
|
|
|
+ - `internal/logic/perm/permListLogic.go` — 可传入任意 `productCode` 查看其他产品权限
|
|
|
+ - `internal/logic/member/memberListLogic.go` — 可传入任意 `productCode` 查看其他产品成员
|
|
|
+- **描述**:上述接口只经过 `JwtAuth` 中间件校验了登录态,但**没有校验调用者是否属于目标产品、是否有权访问该数据**。一个产品 A 的普通成员(MEMBER),可以通过构造请求查看产品 B 的角色、权限、成员信息。
|
|
|
+- **影响**:**信息泄露**。权限系统本身的数据(角色名称、权限列表、用户列表、成员关系)被无差别暴露给所有已登录用户,违反了多产品之间的数据隔离原则。
|
|
|
+- **修复方案**:对含 `productCode` 参数的查询接口,增加产品归属校验:
|
|
|
+
|
|
|
+```go
|
|
|
+// 在 roleListLogic.go 等接口中增加
|
|
|
+caller := middleware.GetUserDetails(l.ctx)
|
|
|
+if caller == nil {
|
|
|
+ return nil, response.ErrUnauthorized("未登录")
|
|
|
+}
|
|
|
+if !caller.IsSuperAdmin {
|
|
|
+ if caller.ProductCode != req.ProductCode {
|
|
|
+ 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("不能通过此接口修改超级管理员的状态")
|
|
|
- }
|
|
|
- }
|
|
|
- ```
|
|
|
+对 `UserDetail` 和 `UserList`,应限制非超管用户只能查看自己所在产品的成员。
|
|
|
|
|
|
---
|
|
|
|
|
|
-### H-3:产品成员被禁用后仍可正常登录
|
|
|
+### H4. UpdateUser 权限校验与同类接口不一致 —— 超管可被越权修改
|
|
|
|
|
|
-- **描述**:`sys_product_member` 表有 `status` 字段(1=启用,2=禁用),但登录流程和 UserDetailsLoader 均未校验此字段。
|
|
|
+- **文件**:`internal/logic/user/updateUserLogic.go:31-45`
|
|
|
+- **描述**:`UpdateUser` 的权限逻辑为「只能改自己,或者超管改别人」。但对比 `UpdateUserStatus`(使用了 `CheckManageAccess` 检查部门层级和权限等级),`UpdateUser` 缺少以下校验:
|
|
|
+ 1. **超管 A 可以修改超管 B 的信息**(包括部门、状态),没有类似 `UpdateUserStatus` 中 "不能修改超级管理员的状态" 的保护。
|
|
|
+ 2. **没有 `CheckManageAccess`**:不校验部门层级关系和 `permsLevel`,超管可以直接修改任何用户的部门归属(`DeptId`),这可能绕过部门层级隔离的安全模型。
|
|
|
+- **影响**:如果系统中有多个超级管理员,超管 A 可以将超管 B 的状态改为 `StatusDisabled`(冻结),或将其部门改为下级部门从而降低其管理范围。
|
|
|
+- **修复方案**:
|
|
|
|
|
|
- - `loginLogic.go` 第 67 行仅检查成员是否存在,不检查 status:
|
|
|
- ```go
|
|
|
- if _, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(
|
|
|
- l.ctx, req.ProductCode, u.Id); memberErr != nil {
|
|
|
- return nil, response.ErrForbidden("您不是该产品的成员")
|
|
|
+```go
|
|
|
+// 对非自身操作增加更严格的校验
|
|
|
+if caller.UserId != req.Id {
|
|
|
+ // 仅超管可操作
|
|
|
+ if !caller.IsSuperAdmin {
|
|
|
+ return response.ErrForbidden("仅超管可修改其他用户信息")
|
|
|
}
|
|
|
- ```
|
|
|
- - `permserver.go` 第 140 行 gRPC Login 同样如此
|
|
|
- - `userDetailsLoader.go` 的 `loadMembership` 方法也不检查 `member.Status`
|
|
|
-
|
|
|
-- **影响**:管理员将某个成员禁用后,该成员仍然可以正常登录、获取 Token、使用该产品的所有权限,禁用操作形同虚设。
|
|
|
-
|
|
|
-- **修复方案**:
|
|
|
+ // 不允许通过此接口修改其他超管
|
|
|
+ if user.IsSuperAdmin == consts.IsSuperAdminYes {
|
|
|
+ if req.Status != 0 || req.DeptId != nil {
|
|
|
+ return response.ErrForbidden("不能修改其他超级管理员的状态和部门")
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
|
|
|
- 在 `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("您在该产品下的成员资格已被禁用")
|
|
|
- }
|
|
|
- ```
|
|
|
+### H5. BindRolePerms 未对 PermIds 去重 —— 重复 ID 导致数据库约束错误
|
|
|
|
|
|
- 在 `userDetailsLoader.go` 的 `loadMembership` 中增加状态检查:
|
|
|
+- **文件**:`internal/logic/role/bindRolePermsLogic.go:41-54`
|
|
|
+- **描述**:`BindRoles` 接口对 `RoleIds` 做了去重处理(第 47-57 行),但 `BindRolePerms` 没有对 `PermIds` 做同样的去重。当客户端传入重复的 `PermIds`(如 `[1, 1, 2]`)时:
|
|
|
+ - `FindByIds` 返回去重后的 2 条记录
|
|
|
+ - `len(perms) != len(req.PermIds)` → `2 != 3` → 返回「包含无效的权限ID」
|
|
|
+ - 错误信息具有误导性,实际上权限 ID 都是有效的,只是有重复
|
|
|
+ - 如果绕过该检查(例如未来修改了校验逻辑),`BatchInsertWithTx` 会因 `UNIQUE KEY uk_role_perm (roleId, permId)` 约束而报错
|
|
|
+- **影响**:前端传入重复数据时,用户收到令人困惑的错误提示,体验差且难以排查。
|
|
|
+- **修复方案**:
|
|
|
|
|
|
- ```go
|
|
|
- if member.Status != consts.StatusEnabled {
|
|
|
- return // 禁用的成员视为无成员身份
|
|
|
- }
|
|
|
- ud.MemberType = member.MemberType
|
|
|
- ```
|
|
|
+```go
|
|
|
+// 在 BindRolePerms 方法开头增加去重逻辑(同 BindRoles 的处理方式)
|
|
|
+if len(req.PermIds) > 0 {
|
|
|
+ seen := make(map[int64]bool, len(req.PermIds))
|
|
|
+ uniqueIds := make([]int64, 0, len(req.PermIds))
|
|
|
+ for _, id := range req.PermIds {
|
|
|
+ if !seen[id] {
|
|
|
+ seen[id] = true
|
|
|
+ uniqueIds = append(uniqueIds, id)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ req.PermIds = uniqueIds
|
|
|
+}
|
|
|
+```
|
|
|
|
|
|
---
|
|
|
|
|
|
-### 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
|
|
|
- }
|
|
|
- ```
|
|
|
-
|
|
|
----
|
|
|
+### H6. SyncPerms 接口缺乏鉴权强度 —— 仅靠 LoginRateLimit 保护
|
|
|
|
|
|
-### 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/handler/routes.go:176-188` + `internal/logic/pub/syncPermsLogic.go`
|
|
|
+- **描述**:`/api/perm/sync` 接口被分配到 `LoginRateLimit` 中间件组(而非 `JwtAuth`),且由于 H2 中的限流失效问题,该接口实际上**几乎没有任何访问频率限制**。虽然接口内部使用 `appKey + appSecret` 做认证,但:
|
|
|
+ - `appKey` 和 `appSecret` 是长期有效的静态凭证
|
|
|
+ - 没有 IP 白名单、签名时间戳、Nonce 等额外防重放机制
|
|
|
+ - 攻击者获取凭证后可无限次调用,覆盖或禁用产品的所有权限
|
|
|
+- **影响**:一旦 `appKey/appSecret` 泄露,攻击者可以:
|
|
|
+ - 传入空的 `Perms` 列表,将目标产品**所有权限禁用**
|
|
|
+ - 注入恶意权限 Code,污染权限数据
|
|
|
+- **修复方案**:
|
|
|
+ 1. 为 SyncPerms 接口增加独立的限流策略(区别于登录限流)
|
|
|
+ 2. 考虑增加请求签名(`timestamp + nonce + HMAC(appSecret, body)`)防重放
|
|
|
+ 3. 在运维层面增加 IP 白名单
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -239,238 +181,144 @@
|
|
|
|
|
|
---
|
|
|
|
|
|
-### M-1:Rate Limiter IP 提取可被伪造
|
|
|
-
|
|
|
-- **描述**:`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")
|
|
|
- }
|
|
|
- ```
|
|
|
-
|
|
|
- `X-Forwarded-For` 头可以被客户端直接伪造。如果服务不在可信反向代理后面,攻击者可以在每次请求中设置不同的 `X-Forwarded-For` 值来绕过速率限制。
|
|
|
-
|
|
|
-- **影响**:登录接口的速率限制可被绕过,使暴力破解攻击成为可能。
|
|
|
-
|
|
|
-- **修复方案**:如果部署在反向代理后,应提取 `X-Forwarded-For` 中的**第一个非信任 IP**;如果直接对外暴露,应优先使用 `RemoteAddr`。建议根据部署拓扑配置信任代理列表:
|
|
|
+### M1. 配置文件明文存储敏感信息
|
|
|
|
|
|
- ```go
|
|
|
- ip := extractRealIP(r, trustedProxies)
|
|
|
- ```
|
|
|
+- **文件**:`etc/perm-api-dev.yaml`(及其他环境配置)
|
|
|
+- **描述**:MySQL 密码、Redis 密码、JWT Secret、ManagementKey 均以明文存储在 YAML 文件中。如果这些文件被提交到 Git 仓库,所有有仓库访问权限的人都能获取生产环境密钥。
|
|
|
+- **建议**:
|
|
|
+ - 生产环境使用环境变量注入或密钥管理服务(如 Vault、AWS Secrets Manager)
|
|
|
+ - 开发环境配置加入 `.gitignore`,仅保留 `perm-api-example.yaml` 模板
|
|
|
|
|
|
---
|
|
|
|
|
|
-### M-2:RefreshToken 端点无速率限制
|
|
|
+### M2. CreateUser 不会自动关联产品成员 —— 业务流程断裂
|
|
|
|
|
|
-- **描述**:`/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"),
|
|
|
- )
|
|
|
- ```
|
|
|
-
|
|
|
-- **影响**:如果 RefreshToken 泄漏,攻击者可以无限次调用此端点,持续生成新的 AccessToken,且无法通过限流缓解。
|
|
|
-
|
|
|
-- **修复方案**:为 RefreshToken 端点增加独立的速率限制,基于 token 中的 userId 进行限流。
|
|
|
+- **文件**:`internal/logic/user/createUserLogic.go`
|
|
|
+- **描述**:`CreateUser` 接口要求 `RequireProductAdminFor(productCode)` 校验调用者是产品管理员,但创建用户后**并不自动将新用户加入该产品**。调用方需要额外调用 `AddMember` 接口,形成两步操作。
|
|
|
+- **影响**:
|
|
|
+ - 如果 `CreateUser` 成功但 `AddMember` 失败(网络中断、前端 Bug),系统中会出现"孤儿用户"——用户存在但不属于任何产品,无法登录任何产品
|
|
|
+ - 增加了前端集成的复杂度和出错概率
|
|
|
+- **建议**:在 `CreateUser` 事务中同时插入 `sys_product_member` 记录,或者至少返回一个明确的提示告知前端需要调用 AddMember。
|
|
|
|
|
|
---
|
|
|
|
|
|
-### M-3:部门禁用后不影响其下用户的登录和权限
|
|
|
-
|
|
|
-- **描述**:`sys_dept` 表有 `status` 字段,但整个系统中没有任何地方在加载用户信息时检查其所属部门是否处于启用状态。
|
|
|
-
|
|
|
- - `userDetailsLoader.go` 的 `loadDept` 不检查部门状态
|
|
|
- - `loadPerms` 中依赖 `ud.DeptType == consts.DeptTypeDev` 判断是否授予全量权限,也不检查部门是否被禁用
|
|
|
- - 一个被禁用的 DEV 类型部门下的用户仍然会获得该产品的全部权限
|
|
|
-
|
|
|
-- **影响**:管理员禁用部门后,预期该部门下用户的权限应受到影响(至少 DEV 部门的自动全权限应该失效),但实际上没有任何效果。
|
|
|
+### M3. Model 初始化修改包级变量 —— 非线程安全
|
|
|
|
|
|
-- **修复方案**:在 `loadPerms` 中判断 `DeptType` 时,增加部门状态检查:
|
|
|
-
|
|
|
- ```go
|
|
|
- if ud.IsSuperAdmin ||
|
|
|
- ud.MemberType == consts.MemberTypeAdmin ||
|
|
|
- ud.MemberType == consts.MemberTypeDeveloper ||
|
|
|
- (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
|
|
|
- // 全量权限
|
|
|
- }
|
|
|
- ```
|
|
|
+- **文件**:`internal/model/user/sysUserModel_gen.go:75-80`(所有 model 的 `_gen.go` 均有此问题)
|
|
|
+- **描述**:`newSysUserModel()` 函数在初始化时会修改包级变量 `cacheSysUserIdPrefix` 和 `cacheSysUserUsernamePrefix`。这些变量在包加载时已有初始值,被函数调用覆写。
|
|
|
+ - 虽然当前代码只在 `NewModels()` 中调用一次,不会出现并发问题
|
|
|
+ - 但作为生成代码模板,如果未来存在多实例或单测并行场景,会产生数据竞争
|
|
|
+- **建议**:将 cache prefix 存储在 struct 实例中,而非修改包级变量。由于这是 goctl 生成代码,建议修改 `cli/goctl/model/model-new.tpl` 模板。
|
|
|
|
|
|
---
|
|
|
|
|
|
-### 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`(清除某产品所有用户缓存)调用。
|
|
|
+### M4. gRPC Login 的限流器可能为 nil
|
|
|
|
|
|
-- **影响**:如果未来 Redis 切换为 Cluster 模式,`SCAN` 只能在单个节点上执行,无法跨节点匹配 key,导致缓存无法正确失效,引发权限数据过期不清除问题。
|
|
|
-
|
|
|
-- **修复方案**:使用 Redis Hash Tag `{tag}` 将相关 key 路由到同一 slot,或改用主动记录关联 key 的方式(如维护一个 Set 记录用户关联的所有缓存 key),清除时通过 Set 成员精确删除。
|
|
|
+- **文件**:`internal/server/permserver.go:116-125`
|
|
|
+- **描述**:`Login` 方法中有 `if s.svcCtx.GrpcLoginLimiter != nil` 的判断,说明设计上允许 limiter 为 nil。但在 `servicecontext.go:30` 中 limiter 总是被创建。如果未来配置变更导致 Redis 不可用,limiter 创建会 panic(`redis.MustNewRedis`),而非优雅降级。
|
|
|
+- **建议**:与当前实现保持一致,确保 `GrpcLoginLimiter` 始终非 nil,或在 `NewServiceContext` 中做容错处理。
|
|
|
|
|
|
---
|
|
|
|
|
|
-### M-5:HTTP 与 gRPC 登录逻辑高度重复
|
|
|
-
|
|
|
-- **描述**:`loginLogic.go` 和 `permserver.go Login` 中的登录逻辑几乎完全相同(校验用户名密码、检查状态、检查产品、检查成员关系、生成 Token),但分别独立实现。
|
|
|
-
|
|
|
-- **影响**:当修复上述 H-3(成员状态检查)等问题时,需要同时修改两处代码,容易遗漏。未来任何登录逻辑的变更都需要双重维护。
|
|
|
+### M5. UserDetailsLoader.loadPerms 中研发部门判定可能不符合预期
|
|
|
|
|
|
-- **修复方案**:将核心登录逻辑抽取为共享的 service 方法,HTTP handler 和 gRPC server 都调用同一个方法。
|
|
|
+- **文件**:`internal/loaders/userDetailsLoader.go:312-316`
|
|
|
+- **描述**:
|
|
|
|
|
|
----
|
|
|
+```go
|
|
|
+if ud.IsSuperAdmin ||
|
|
|
+ ud.MemberType == consts.MemberTypeAdmin ||
|
|
|
+ ud.MemberType == consts.MemberTypeDeveloper ||
|
|
|
+ (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
|
|
|
+```
|
|
|
|
|
|
-## ⚠️ 健壮性与性能建议 (Low)
|
|
|
+ 研发部门(`DeptType == "DEV"`)的判定**不与 productCode 关联**——只要用户所在部门类型是 `DEV` 且部门启用,该用户在**所有产品**下都自动拥有全量权限。这意味着一个被拉进产品 A 的 MEMBER 类型成员,如果碰巧在研发部门,他在产品 A 下拥有的权限和 ADMIN 一样。
|
|
|
+- **影响**:研发部门成员的权限范围可能超出业务预期,与成员类型(MEMBER)赋予的权限不匹配。
|
|
|
+- **建议**:确认此行为是否为设计意图。如果研发部门全量权限仅应作用于特定产品,需增加产品关联判断。
|
|
|
|
|
|
---
|
|
|
|
|
|
-### L-1:SyncPerms 接口无速率限制
|
|
|
-
|
|
|
-- **描述**:`/api/perm/sync` 端点无任何中间件保护(无 JWT、无 RateLimit),仅靠 `appKey + appSecret` 认证。虽然密钥泄漏的概率较低,但一旦泄漏,攻击者可以高频调用此接口触发大量数据库写操作和缓存清除。
|
|
|
+### M6. RefreshToken 不会续签 refreshToken 本身
|
|
|
|
|
|
-- **修复方案**:为 SyncPerms 增加基于 appKey 的速率限制。
|
|
|
+- **文件**:`internal/logic/pub/refreshTokenLogic.go:67-68`
|
|
|
+- **描述**:`RefreshToken` 接口返回新的 `accessToken`,但**原样返回旧的 `refreshToken`**。随着时间推移,refreshToken 会过期(7 天),用户被迫重新登录。
|
|
|
+- **影响**:对于需要长期保持登录状态的场景(如桌面客户端、后台管理系统),用户体验不佳——每 7 天必须重新输入密码。
|
|
|
+- **建议**:根据业务需求决定是否在每次刷新时签发新的 refreshToken(滑动过期策略)。如果不续签,应在 API 文档中明确说明 refreshToken 有效期为固定 7 天。
|
|
|
|
|
|
---
|
|
|
|
|
|
-### L-2:CreateProduct 响应暴露 AppSecret 明文
|
|
|
-
|
|
|
-- **描述**:`createProductLogic.go` 创建产品后将 `AppSecret` 明文返回给前端,且 AppSecret 在数据库中也以明文存储。
|
|
|
-
|
|
|
- ```go
|
|
|
- return &types.CreateProductResp{
|
|
|
- AppSecret: appSecret, // 明文返回
|
|
|
- AdminPassword: adminPassword, // 管理员初始密码明文返回
|
|
|
- }
|
|
|
- ```
|
|
|
+### M7. UserDetailsLoader 缓存清理使用 SCAN —— 性能与兼容性风险
|
|
|
|
|
|
-- **影响**:AppSecret 相当于产品的 API 密钥,一旦通过网络被截获或日志记录,将可被用于调用 SyncPerms 等无需登录的接口。AdminPassword 同理。
|
|
|
-
|
|
|
-- **修复方案**:这是一次性展示场景(创建后只展示一次),风险可控。建议:
|
|
|
- 1. 确保传输层使用 HTTPS
|
|
|
- 2. 确认日志中不会记录响应体(当前 `response.go` 的错误处理不会记录成功响应体,但需确认 go-zero 框架层面的 access log 配置)
|
|
|
- 3. AppSecret 数据库存储可考虑改为哈希存储,验证时对比哈希值
|
|
|
+- **文件**:`internal/loaders/userDetailsLoader.go:162-180`
|
|
|
+- **描述**:`cleanByPattern` 使用 `SCAN` 命令按 pattern 匹配并删除缓存 key。代码注释中已标注此方法不兼容 Redis Cluster。此外:
|
|
|
+ - `CleanByProduct` 使用 pattern `*:ud:*:{productCode}`,在产品成员较多时可能扫描大量 key
|
|
|
+ - `UpdateDept` 中对每个子部门的每个用户逐个调用 `Clean`,如果部门内有几十个用户,会产生多次 SCAN 操作
|
|
|
+- **建议**:
|
|
|
+ - 如果确定使用单节点 Redis,当前实现可接受
|
|
|
+ - 若考虑未来迁移到 Redis Cluster,建议使用 Hash Tag(如 `{productCode}:ud:userId`)或维护一个 Set 记录某个产品下的所有缓存 key,以支持批量删除
|
|
|
|
|
|
---
|
|
|
|
|
|
-### L-3:UserDetailsLoader.Load 中 singleflight 的 panic 风险
|
|
|
-
|
|
|
-- **描述**:`userDetailsLoader.go` 第 106-118 行:
|
|
|
-
|
|
|
- ```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
|
|
|
- ```
|
|
|
+### M8. 部分接口缺少输入长度校验
|
|
|
|
|
|
- `loadFromDB` 在 `loadUser` 失败时返回 `(ud, false)`,此时 `ud` 不是 nil(是一个初始化过的 `&UserDetails{}`),所以实际上不会 panic。但如果未来重构时 `loadFromDB` 的返回值逻辑变化,此处的类型断言缺乏安全检查。
|
|
|
+- **文件**:各 `createXxxLogic.go`、`updateXxxLogic.go`
|
|
|
+- **描述**:以下字段没有长度校验,但数据库有 `varchar` 长度限制:
|
|
|
+ - `username`(最大 64)、`nickname`(最大 64)、`email`(最大 64)
|
|
|
+ - `productCode`(最大 64)、`productName`(最大 64)
|
|
|
+ - `roleName`(最大 64)、`remark`(最大 255)
|
|
|
+ - `dept.name`(最大 64)、`dept.path`(最大 512)
|
|
|
+
|
|
|
+ 当前未做前端/后端长度校验,超长输入会直接触发 MySQL 的 `Data too long` 错误(1406),返回不友好的 500 错误。
|
|
|
+- **建议**:在 Logic 层统一增加关键字段的长度校验,返回可读的 400 错误信息。
|
|
|
|
|
|
-- **修复方案**:使用安全类型断言:
|
|
|
+---
|
|
|
|
|
|
- ```go
|
|
|
- ud, ok := v.(*UserDetails)
|
|
|
- if !ok || ud == nil {
|
|
|
- return &UserDetails{UserId: userId, ProductCode: productCode}
|
|
|
- }
|
|
|
- return ud
|
|
|
- ```
|
|
|
+## 💡 低风险优化建议 (Low)
|
|
|
|
|
|
---
|
|
|
|
|
|
-### L-4:BindRolesLogic 绑定角色未检查目标用户是否为产品成员
|
|
|
-
|
|
|
-- **描述**:`bindRolesLogic.go` 在为用户绑定角色时,只检查用户是否存在和角色是否属于当前产品,但**没有检查目标用户是否是当前产品的成员**。
|
|
|
+### L1. JWT Claims 中存储完整权限列表可能导致 Token 膨胀
|
|
|
|
|
|
- ```go
|
|
|
- if _, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.UserId); err != nil {
|
|
|
- return response.ErrNotFound("用户不存在")
|
|
|
- }
|
|
|
- // 缺少:检查用户是否为当前产品成员
|
|
|
- ```
|
|
|
+- **文件**:`internal/logic/auth/jwt.go:22-38`
|
|
|
+- **描述**:`Claims.Perms` 是 `[]string`,权限 Code 字符串数组被完整编码进 JWT。如果某个产品配置了数百个权限,Token 可能达到数 KB 甚至超过 HTTP Header 限制(通常 8KB)。
|
|
|
+- **建议**:考虑只在 JWT 中存储必要标识(userId、productCode、memberType),权限列表由服务端通过 `UserDetailsLoader` 实时获取(当前中间件已经在这样做)。
|
|
|
|
|
|
-- **影响**:可以为非产品成员的用户绑定角色,这些角色在 `loadRoles` 时会被加载但因用户不是成员所以 `loadMembership` 不会设置 MemberType,权限不会实际生效。但数据库中会存在孤立的无效关联数据。
|
|
|
+### L2. DeptTree 返回所有部门包含已禁用的
|
|
|
|
|
|
-- **修复方案**:在绑定前校验目标用户是否为当前产品成员。
|
|
|
+- **文件**:`internal/logic/dept/deptTreeLogic.go:27`
|
|
|
+- **描述**:`FindAll` 查询所有部门不区分状态,禁用的部门也会出现在树中。
|
|
|
+- **建议**:根据业务需求,可增加参数控制是否过滤禁用部门,或在返回中标注状态供前端处理。
|
|
|
|
|
|
----
|
|
|
+### L3. CreateProduct 返回明文 AdminPassword
|
|
|
|
|
|
-### L-5:SetUserPerms 同理未校验产品成员关系
|
|
|
+- **文件**:`internal/logic/product/createProductLogic.go:116-124`
|
|
|
+- **描述**:创建产品时自动生成的管理员密码在 HTTP 响应中明文返回。若响应被日志系统记录(如 access log、网关日志),密码可能泄露。
|
|
|
+- **建议**:确保 API 网关/日志系统不记录响应体,或改为邮件/消息通知的方式下发初始密码。
|
|
|
|
|
|
-- **描述**:与 L-4 类似,`setUserPermsLogic.go` 在设置用户权限时没有检查目标用户是否为当前产品的成员,可以为非成员用户设置权限(虽然实际不会生效)。
|
|
|
+### L4. 错误处理中 `errors.As` 与 `==` 混用
|
|
|
|
|
|
-- **修复方案**:在设置前校验目标用户是否为当前产品成员。
|
|
|
+- **文件**:`internal/logic/pub/loginService.go:33` 使用 `== user.ErrNotFound`,而 `response.go:47` 使用 `errors.As`
|
|
|
+- **描述**:`ErrNotFound` 比较使用 `==`(值比较),如果未来 ErrNotFound 被 `fmt.Errorf("%w", ...)` 包装,`==` 会失效。
|
|
|
+- **建议**:统一使用 `errors.Is(err, user.ErrNotFound)` 进行哨兵错误判断。
|
|
|
|
|
|
----
|
|
|
+### L5. ChangePassword 成功后不会使旧 Token 失效
|
|
|
|
|
|
-### L-6:删除部门时忽略了 FindIdsByDeptId 的错误
|
|
|
+- **文件**:`internal/logic/auth/changePasswordLogic.go`
|
|
|
+- **描述**:修改密码后清理了 UserDetails 缓存,但已签发的 Access Token 和 Refresh Token 仍然有效(最长可达 7 天)。如果用户因密码泄露而修改密码,攻击者持有的旧 Token 仍可正常使用。
|
|
|
+- **建议**:引入 Token 版本号(存储在用户记录中),修改密码时递增版本号,中间件校验时比对版本号。
|
|
|
|
|
|
-- **描述**:`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` 指向一个已不存在的部门。
|
|
|
+## 📊 审计总结
|
|
|
|
|
|
-- **修复方案**:处理错误并在查询失败时阻止删除:
|
|
|
+| 级别 | 数量 | 关键词 |
|
|
|
+|------|------|--------|
|
|
|
+| 🚩 High | 6 | 越权登录、限流失效、水平越权、权限校验不一致、数据校验缺失、接口防护不足 |
|
|
|
+| ⚠️ Medium | 8 | 明文密钥、流程断裂、线程安全、缓存一致性、权限范围、输入校验 |
|
|
|
+| 💡 Low | 5 | Token 膨胀、状态过滤、明文密码返回、错误处理、Token 吊销 |
|
|
|
|
|
|
- ```go
|
|
|
- userIds, err := l.svcCtx.SysUserModel.FindIdsByDeptId(l.ctx, req.Id)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- ```
|
|
|
+**优先修复建议**:H1(管理后台越权)→ H2(限流失效)→ H3(水平越权)→ H4(权限不一致)→ M1(密钥管理)→ H5/H6
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 📋 审计总结
|
|
|
-
|
|
|
-| 级别 | 编号 | 问题摘要 | 影响面 |
|
|
|
-|------|------|----------|--------|
|
|
|
-| 🔴 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(禁用成员仍可登录)次之,因为管理员执行禁用操作后会产生"已生效"的错觉。
|
|
|
+*本报告基于静态代码审计,未涉及运行时测试和渗透测试。建议在修复后进行集成测试验证。*
|