# 权限管理系统 - 深度代码审计报告 > 审计范围:`internal/` 下所有非测试源代码,包括 logic、model、middleware、handler、config、loader、server 层 > 审计时间:2026-04-17 > 排除范围:`*_test.go`、`*_mock_test.go`、`testutil/`、`cli/`、`pb/`(生成代码) --- ## 🚩 核心逻辑漏洞 (High Risk) --- ### H1. AdminLogin 未校验用户是否为超级管理员 —— 越权访问风险 - **文件**:`internal/logic/pub/adminLoginLogic.go:33-91` - **描述**:`AdminLogin` 接口仅校验了 `ManagementKey` 和用户名密码,但**没有校验用户的 `isSuperAdmin` 字段**。任何普通用户只要知道 `ManagementKey`,就能通过管理后台登录接口获取一个 `productCode=""` 的 Token。 - **影响**: - 拿到此 Token 后,用户通过 JWT 中间件校验,可以调用所有 `JwtAuth` 保护的接口。 - 虽然 `RequireSuperAdmin()` 会在创建产品、管理部门等操作中拦截,但 `UserDetail`、`UserList`、`RoleList`、`ProductList`、`DeptTree` 等查询接口**没有额外权限校验**,非超管用户可以通过此途径浏览所有系统数据。 - `ManagementKey` 一旦泄露(如被抓包、配置文件泄露),整个系统对该用户门户大开。 - **修复方案**: ```go // adminLoginLogic.go — 在密码验证通过后增加超管校验 if u.IsSuperAdmin != consts.IsSuperAdminYes { return nil, response.ErrForbidden("仅超级管理员可通过管理后台登录") } ``` --- ### H2. 限流中间件 IP 提取逻辑有两个严重缺陷 - **文件**:`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 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) } } 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 } ``` --- ### 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("无权访问该产品的数据") } } ``` 对 `UserDetail` 和 `UserList`,应限制非超管用户只能查看自己所在产品的成员。 --- ### H4. UpdateUser 权限校验与同类接口不一致 —— 超管可被越权修改 - **文件**:`internal/logic/user/updateUserLogic.go:31-45` - **描述**:`UpdateUser` 的权限逻辑为「只能改自己,或者超管改别人」。但对比 `UpdateUserStatus`(使用了 `CheckManageAccess` 检查部门层级和权限等级),`UpdateUser` 缺少以下校验: 1. **超管 A 可以修改超管 B 的信息**(包括部门、状态),没有类似 `UpdateUserStatus` 中 "不能修改超级管理员的状态" 的保护。 2. **没有 `CheckManageAccess`**:不校验部门层级关系和 `permsLevel`,超管可以直接修改任何用户的部门归属(`DeptId`),这可能绕过部门层级隔离的安全模型。 - **影响**:如果系统中有多个超级管理员,超管 A 可以将超管 B 的状态改为 `StatusDisabled`(冻结),或将其部门改为下级部门从而降低其管理范围。 - **修复方案**: ```go // 对非自身操作增加更严格的校验 if caller.UserId != req.Id { // 仅超管可操作 if !caller.IsSuperAdmin { return response.ErrForbidden("仅超管可修改其他用户信息") } // 不允许通过此接口修改其他超管 if user.IsSuperAdmin == consts.IsSuperAdminYes { if req.Status != 0 || req.DeptId != nil { return response.ErrForbidden("不能修改其他超级管理员的状态和部门") } } } ``` --- ### H5. BindRolePerms 未对 PermIds 去重 —— 重复 ID 导致数据库约束错误 - **文件**:`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 // 在 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 } ``` --- ### H6. SyncPerms 接口缺乏鉴权强度 —— 仅靠 LoginRateLimit 保护 - **文件**:`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 白名单 --- ## ⚠️ 健壮性与性能建议 (Medium) --- ### M1. 配置文件明文存储敏感信息 - **文件**:`etc/perm-api-dev.yaml`(及其他环境配置) - **描述**:MySQL 密码、Redis 密码、JWT Secret、ManagementKey 均以明文存储在 YAML 文件中。如果这些文件被提交到 Git 仓库,所有有仓库访问权限的人都能获取生产环境密钥。 - **建议**: - 生产环境使用环境变量注入或密钥管理服务(如 Vault、AWS Secrets Manager) - 开发环境配置加入 `.gitignore`,仅保留 `perm-api-example.yaml` 模板 --- ### M2. CreateUser 不会自动关联产品成员 —— 业务流程断裂 - **文件**:`internal/logic/user/createUserLogic.go` - **描述**:`CreateUser` 接口要求 `RequireProductAdminFor(productCode)` 校验调用者是产品管理员,但创建用户后**并不自动将新用户加入该产品**。调用方需要额外调用 `AddMember` 接口,形成两步操作。 - **影响**: - 如果 `CreateUser` 成功但 `AddMember` 失败(网络中断、前端 Bug),系统中会出现"孤儿用户"——用户存在但不属于任何产品,无法登录任何产品 - 增加了前端集成的复杂度和出错概率 - **建议**:在 `CreateUser` 事务中同时插入 `sys_product_member` 记录,或者至少返回一个明确的提示告知前端需要调用 AddMember。 --- ### M3. Model 初始化修改包级变量 —— 非线程安全 - **文件**:`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` 模板。 --- ### M4. gRPC Login 的限流器可能为 nil - **文件**:`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` 中做容错处理。 --- ### M5. UserDetailsLoader.loadPerms 中研发部门判定可能不符合预期 - **文件**:`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) { ``` 研发部门(`DeptType == "DEV"`)的判定**不与 productCode 关联**——只要用户所在部门类型是 `DEV` 且部门启用,该用户在**所有产品**下都自动拥有全量权限。这意味着一个被拉进产品 A 的 MEMBER 类型成员,如果碰巧在研发部门,他在产品 A 下拥有的权限和 ADMIN 一样。 - **影响**:研发部门成员的权限范围可能超出业务预期,与成员类型(MEMBER)赋予的权限不匹配。 - **建议**:确认此行为是否为设计意图。如果研发部门全量权限仅应作用于特定产品,需增加产品关联判断。 --- ### M6. RefreshToken 不会续签 refreshToken 本身 - **文件**:`internal/logic/pub/refreshTokenLogic.go:67-68` - **描述**:`RefreshToken` 接口返回新的 `accessToken`,但**原样返回旧的 `refreshToken`**。随着时间推移,refreshToken 会过期(7 天),用户被迫重新登录。 - **影响**:对于需要长期保持登录状态的场景(如桌面客户端、后台管理系统),用户体验不佳——每 7 天必须重新输入密码。 - **建议**:根据业务需求决定是否在每次刷新时签发新的 refreshToken(滑动过期策略)。如果不续签,应在 API 文档中明确说明 refreshToken 有效期为固定 7 天。 --- ### M7. UserDetailsLoader 缓存清理使用 SCAN —— 性能与兼容性风险 - **文件**:`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,以支持批量删除 --- ### M8. 部分接口缺少输入长度校验 - **文件**:各 `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 错误信息。 --- ## 💡 低风险优化建议 (Low) --- ### L1. JWT Claims 中存储完整权限列表可能导致 Token 膨胀 - **文件**:`internal/logic/auth/jwt.go:22-38` - **描述**:`Claims.Perms` 是 `[]string`,权限 Code 字符串数组被完整编码进 JWT。如果某个产品配置了数百个权限,Token 可能达到数 KB 甚至超过 HTTP Header 限制(通常 8KB)。 - **建议**:考虑只在 JWT 中存储必要标识(userId、productCode、memberType),权限列表由服务端通过 `UserDetailsLoader` 实时获取(当前中间件已经在这样做)。 ### L2. DeptTree 返回所有部门包含已禁用的 - **文件**:`internal/logic/dept/deptTreeLogic.go:27` - **描述**:`FindAll` 查询所有部门不区分状态,禁用的部门也会出现在树中。 - **建议**:根据业务需求,可增加参数控制是否过滤禁用部门,或在返回中标注状态供前端处理。 ### L3. CreateProduct 返回明文 AdminPassword - **文件**:`internal/logic/product/createProductLogic.go:116-124` - **描述**:创建产品时自动生成的管理员密码在 HTTP 响应中明文返回。若响应被日志系统记录(如 access log、网关日志),密码可能泄露。 - **建议**:确保 API 网关/日志系统不记录响应体,或改为邮件/消息通知的方式下发初始密码。 ### 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 失效 - **文件**:`internal/logic/auth/changePasswordLogic.go` - **描述**:修改密码后清理了 UserDetails 缓存,但已签发的 Access Token 和 Refresh Token 仍然有效(最长可达 7 天)。如果用户因密码泄露而修改密码,攻击者持有的旧 Token 仍可正常使用。 - **建议**:引入 Token 版本号(存储在用户记录中),修改密码时递增版本号,中间件校验时比对版本号。 --- ## 📊 审计总结 | 级别 | 数量 | 关键词 | |------|------|--------| | 🚩 High | 6 | 越权登录、限流失效、水平越权、权限校验不一致、数据校验缺失、接口防护不足 | | ⚠️ Medium | 8 | 明文密钥、流程断裂、线程安全、缓存一致性、权限范围、输入校验 | | 💡 Low | 5 | Token 膨胀、状态过滤、明文密码返回、错误处理、Token 吊销 | **优先修复建议**:H1(管理后台越权)→ H2(限流失效)→ H3(水平越权)→ H4(权限不一致)→ M1(密钥管理)→ H5/H6 --- *本报告基于静态代码审计,未涉及运行时测试和渗透测试。建议在修复后进行集成测试验证。*