审计范围:
internal/下所有非测试源代码,包括 logic、model、middleware、handler、config、loader、server 层
审计时间:2026-04-17
排除范围:*_test.go、*_mock_test.go、testutil/、cli/、pb/(生成代码)
internal/logic/pub/adminLoginLogic.go:33-91AdminLogin 接口仅校验了 ManagementKey 和用户名密码,但没有校验用户的 isSuperAdmin 字段。任何普通用户只要知道 ManagementKey,就能通过管理后台登录接口获取一个 productCode="" 的 Token。JwtAuth 保护的接口。RequireSuperAdmin() 会在创建产品、管理部门等操作中拦截,但 UserDetail、UserList、RoleList、ProductList、DeptTree 等查询接口没有额外权限校验,非超管用户可以通过此途径浏览所有系统数据。ManagementKey 一旦泄露(如被抓包、配置文件泄露),整个系统对该用户门户大开。// adminLoginLogic.go — 在密码验证通过后增加超管校验
if u.IsSuperAdmin != consts.IsSuperAdminYes {
return nil, response.ErrForbidden("仅超级管理员可通过管理后台登录")
}
internal/middleware/ratelimitMiddleware.go:24-28r.RemoteAddr 包含端口号:Go 的 http.Request.RemoteAddr 格式为 IP:Port(如 192.168.1.1:54321)。由于每个 TCP 连接的临时端口不同,限流 Key 变成了每个连接独立计数,同一个客户端 IP 几乎不可能触发限流。RemoteAddr 是代理服务器的 IP,所有客户端共享同一个限流桶,导致少量正常请求就会触发全局限流。/auth/login、/auth/adminLogin)的暴力破解防护形同虚设。攻击者可以不受限制地进行密码爆破。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
}
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 参数的查询接口,增加产品归属校验:// 在 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,应限制非超管用户只能查看自己所在产品的成员。
internal/logic/user/updateUserLogic.go:31-45UpdateUser 的权限逻辑为「只能改自己,或者超管改别人」。但对比 UpdateUserStatus(使用了 CheckManageAccess 检查部门层级和权限等级),UpdateUser 缺少以下校验:
UpdateUserStatus 中 "不能修改超级管理员的状态" 的保护。CheckManageAccess:不校验部门层级关系和 permsLevel,超管可以直接修改任何用户的部门归属(DeptId),这可能绕过部门层级隔离的安全模型。StatusDisabled(冻结),或将其部门改为下级部门从而降低其管理范围。// 对非自身操作增加更严格的校验
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("不能修改其他超级管理员的状态和部门")
}
}
}
internal/logic/role/bindRolePermsLogic.go:41-54BindRoles 接口对 RoleIds 做了去重处理(第 47-57 行),但 BindRolePerms 没有对 PermIds 做同样的去重。当客户端传入重复的 PermIds(如 [1, 1, 2])时:
FindByIds 返回去重后的 2 条记录len(perms) != len(req.PermIds) → 2 != 3 → 返回「包含无效的权限ID」BatchInsertWithTx 会因 UNIQUE KEY uk_role_perm (roleId, permId) 约束而报错// 在 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
}
internal/handler/routes.go:176-188 + internal/logic/pub/syncPermsLogic.go/api/perm/sync 接口被分配到 LoginRateLimit 中间件组(而非 JwtAuth),且由于 H2 中的限流失效问题,该接口实际上几乎没有任何访问频率限制。虽然接口内部使用 appKey + appSecret 做认证,但:
appKey 和 appSecret 是长期有效的静态凭证appKey/appSecret 泄露,攻击者可以:
Perms 列表,将目标产品所有权限禁用timestamp + nonce + HMAC(appSecret, body))防重放etc/perm-api-dev.yaml(及其他环境配置).gitignore,仅保留 perm-api-example.yaml 模板internal/logic/user/createUserLogic.goCreateUser 接口要求 RequireProductAdminFor(productCode) 校验调用者是产品管理员,但创建用户后并不自动将新用户加入该产品。调用方需要额外调用 AddMember 接口,形成两步操作。CreateUser 成功但 AddMember 失败(网络中断、前端 Bug),系统中会出现"孤儿用户"——用户存在但不属于任何产品,无法登录任何产品CreateUser 事务中同时插入 sys_product_member 记录,或者至少返回一个明确的提示告知前端需要调用 AddMember。internal/model/user/sysUserModel_gen.go:75-80(所有 model 的 _gen.go 均有此问题)newSysUserModel() 函数在初始化时会修改包级变量 cacheSysUserIdPrefix 和 cacheSysUserUsernamePrefix。这些变量在包加载时已有初始值,被函数调用覆写。
NewModels() 中调用一次,不会出现并发问题cli/goctl/model/model-new.tpl 模板。internal/server/permserver.go:116-125Login 方法中有 if s.svcCtx.GrpcLoginLimiter != nil 的判断,说明设计上允许 limiter 为 nil。但在 servicecontext.go:30 中 limiter 总是被创建。如果未来配置变更导致 Redis 不可用,limiter 创建会 panic(redis.MustNewRedis),而非优雅降级。GrpcLoginLimiter 始终非 nil,或在 NewServiceContext 中做容错处理。internal/loaders/userDetailsLoader.go:312-316if 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 一样。
internal/logic/pub/refreshTokenLogic.go:67-68RefreshToken 接口返回新的 accessToken,但原样返回旧的 refreshToken。随着时间推移,refreshToken 会过期(7 天),用户被迫重新登录。internal/loaders/userDetailsLoader.go:162-180cleanByPattern 使用 SCAN 命令按 pattern 匹配并删除缓存 key。代码注释中已标注此方法不兼容 Redis Cluster。此外:
CleanByProduct 使用 pattern *:ud:*:{productCode},在产品成员较多时可能扫描大量 keyUpdateDept 中对每个子部门的每个用户逐个调用 Clean,如果部门内有几十个用户,会产生多次 SCAN 操作{productCode}:ud:userId)或维护一个 Set 记录某个产品下的所有缓存 key,以支持批量删除createXxxLogic.go、updateXxxLogic.govarchar 长度限制:
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 错误。
internal/logic/auth/jwt.go:22-38Claims.Perms 是 []string,权限 Code 字符串数组被完整编码进 JWT。如果某个产品配置了数百个权限,Token 可能达到数 KB 甚至超过 HTTP Header 限制(通常 8KB)。UserDetailsLoader 实时获取(当前中间件已经在这样做)。internal/logic/dept/deptTreeLogic.go:27FindAll 查询所有部门不区分状态,禁用的部门也会出现在树中。internal/logic/product/createProductLogic.go:116-124errors.As 与 == 混用internal/logic/pub/loginService.go:33 使用 == user.ErrNotFound,而 response.go:47 使用 errors.AsErrNotFound 比较使用 ==(值比较),如果未来 ErrNotFound 被 fmt.Errorf("%w", ...) 包装,== 会失效。errors.Is(err, user.ErrNotFound) 进行哨兵错误判断。internal/logic/auth/changePasswordLogic.go| 级别 | 数量 | 关键词 |
|---|---|---|
| 🚩 High | 6 | 越权登录、限流失效、水平越权、权限校验不一致、数据校验缺失、接口防护不足 |
| ⚠️ Medium | 8 | 明文密钥、流程断裂、线程安全、缓存一致性、权限范围、输入校验 |
| 💡 Low | 5 | Token 膨胀、状态过滤、明文密码返回、错误处理、Token 吊销 |
优先修复建议:H1(管理后台越权)→ H2(限流失效)→ H3(水平越权)→ H4(权限不一致)→ M1(密钥管理)→ H5/H6
本报告基于静态代码审计,未涉及运行时测试和渗透测试。建议在修复后进行集成测试验证。