13 KiB
13 KiB
mgt(后台组织与权限管理)代码审计报告
| 项 | 内容 |
|---|---|
| 审计对象 | module/base/mgt |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件约 80 个(cmd/、internal/ 全量、service/、types/、middleware/、libs/);无 proto 定义(纯 Gin REST),无生成代码 |
| 入口 | cmd/main(Gin 单进程)、聚合入口 pkgs/all、pkgs/ecmall |
| 对外协议 | 仅原生 REST,基础前缀 /rest/mgt(README 所述 /mgt/v1/... 与实际不符) |
| 结论摘要 | 鉴权中间件已正确挂载于全部管理路由(JWT + 超级管理员角色双重校验,模块内无绕过);SQL 全部参数化。但 Login 的 AppId/AppKey 路径以明文相等比对密码列(哈希即凭证);dev 配置启用 InitRootUser 注入 root/123456 弱默认账号;列表查询无分页上限;配置文件注释与实现行为矛盾。 |
1. 服务定位与职责
后台 RBAC 权限管理系统:用户、角色、权限(菜单/按钮)、应用、部门的管理与关联。负责管理员账号生命周期、登录认证、Token 刷新、短信重置密码、以及权限/角色/部门树的维护。它是**管理面(admin console)**服务,所有写操作需超级管理员权限。
2. 代码结构与入口
| 路径 | 职责 |
|---|---|
cmd/main/main.go |
进程入口:配置→impl→Gin→session→中间件→routers.Register→监听 |
internal/config/config.go |
配置结构(Databases/SecretKey/InitRootUser 等);设置 JWT 密钥来源 |
internal/impl/{impl,with}.go |
初始化 Redis/DB/Etcd/内存缓存;DB 连接(mysql/postgres)+ 自动迁移 |
internal/middleware/rbac.go |
RequireAdmin / IsSuperAdmin / EnsureSelfOrAdmin 管理员鉴定 |
internal/libs/validator.go |
参数校验(手机号、邮箱自定义规则) |
internal/routers/register.go |
REST 路由注册(匿名组 + 鉴权组 + 管理员组) |
internal/logic/hello/ping.go |
健康检查、Session 调试端点 |
internal/logic/pub/{login,refresh,forget}.go |
公开接口:登录、刷新、短信重置 |
internal/logic/user/* |
用户 CRUD、角色/权限分配、查询 |
internal/logic/role/*、permission/*、application/*、department/* |
各资源 CRUD 与关联 |
internal/logic/tool/tool.go |
分页参数处理 |
internal/models/* |
16+ 模型 + impl.go(迁移与 root 初始化) + init_db.go(root 数据) + init_menu.go(菜单) |
internal/types/{vars,req,resp}.go |
常量、请求/响应结构 |
service/{expose,dependencies}.go |
聚合宿主注入(共用 Engine 与连接) |
与
logs/passport不同,mgt 没有 proto/gRPC,是纯 Gin 服务;聚合入口service.Expose直接复用同一Engine注册路由,鉴权接线一致。
3. 接口清单
鉴权分三层:
anonymous(无)、auth(middleware.JwtAuth)、admin(mgtmw.RequireAdmin)。所有管理写操作均挂在admin组。
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
|---|---|---|---|---|
| GET | /rest/mgt/ping |
健康检查 | 无 | logic/hello/ping.go |
| GET | /rest/mgt/session |
Session 调试 | 无 | logic/hello/ping.go |
| POST | /rest/mgt/login |
登录(账号密码 / AppId+AppKey) | 无 | logic/pub/login.go |
| POST | /rest/mgt/refresh |
刷新 Token | auth | logic/pub/refresh.go |
| POST | /rest/mgt/reset |
短信验证码重置密码 | 无 | logic/pub/forget.go |
| POST | /rest/mgt/user/role /app /pmn /pmn_tree |
当前用户角色/应用/权限查询 | auth | logic/user/* |
| POST | /rest/mgt/user/create /del /detail /modify /fetch /list /set_role /del_role /set_pmn /modify_pmn /del_pmn |
用户管理(全部写操作) | admin | logic/user/* |
| POST | /rest/mgt/app/* |
应用管理 | admin | logic/application/* |
| POST | /rest/mgt/role/* |
角色管理 | admin | logic/role/* |
| POST | /rest/mgt/pmn/* |
权限管理 | admin | logic/permission/* |
| POST | /rest/mgt/dpt/* |
部门管理 | admin | logic/department/* |
对照发现:README 描述的路径前缀
/mgt/v1/...与实际注册前缀/rest/mgt/...不一致;ForgetPwdByPwd(旧密码改密)为空 stub 且未注册路由。
4. 数据模型与表
| 表名 | 字段 | 类型 | 键/约束 | 说明 |
|---|---|---|---|---|
mgt_user |
id |
- | PK | 用户 |
account |
varchar(255) | uniqueIndex | 账号(含 root) |
|
phone |
varchar(255) | - | 手机号 | |
password |
varchar(255) | - | 密码(bcrypt / 兼容 MD5 十六进制) | |
salt |
varchar(255) | - | 密码盐 | |
last_login_at |
time | - | 最后登录 | |
mgt_role |
id/name/description |
- | - | 角色(含 超级管理员) |
mgt_permission |
id/code/title/menu_path/type/app_id/parent_id |
- | - | 权限/菜单 |
mgt_application |
id/workspace/title |
- | workspace 唯一 | 应用 |
mgt_department |
id/name/parent_id/leader_id |
- | - | 部门 |
| 关联表 | mgt_link_user_role mgt_link_user_pmn mgt_link_user_app mgt_link_user_dpt mgt_link_role_pmn mgt_link_role_app mgt_link_dpt_role mgt_link_dpt_pmn |
- | 复合唯一 | 多对多关联 |
5. 核心流程
flowchart TD
A["POST /rest/mgt/login"] --> B["按 account 或 AppId 查 mgt_user"]
B --> C{"AppKey 路径?"}
C -->|否| D["checkPwdAndStatus: bcrypt / MD5 比对"]
C -->|是| E["checkAppKeyAndStatus: 明文相等比对密码列"]
D --> F["签发 JWT"]
E --> F
G["POST /rest/mgt/user/create"] --> H["RequireAdmin 校验超级管理员"]
H --> I["bcrypt 哈希 + 事务建用户/关联"]
J["POST /rest/mgt/reset"] --> K["校验短信验证码(Redis)"]
K --> L["bcrypt 重置密码"]
6. 审计发现
6.1 安全
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/pub/login.go:147-159 |
checkAppKeyAndStatus 以明文相等比对密码列:if userPwd != pwd 直接比较存储的 password 字段与传入 AppKey。对 MD5 兼容老用户(密码列存 md5(pwd+salt) 十六进制),攻击者只要拿到密码哈希,以 app_id=账号、app_key=哈希 即可登录,哈希本身成为可用凭证(pass-the-hash),加密存储失去意义。 |
| 高 | etc/mgt_dev.yaml:16 + internal/models/impl.go:69-70 + internal/models/init_db.go:16 |
弱默认管理员账号:dev 配置 InitRootUser: true,启动时 InitRootUserData() 创建 root 账号、口令硬编码 123456。非生产环境一旦启用即注入后门级默认凭据;代码不强制改密。 |
| 中 | internal/config/config.go:19,24-29 |
配置语义矛盾:注释"未配置时默认 true",但实现 InitRootUser==nil 时返回 false;README:245 又写"默认 false"。部署者若省略该字段,实际不会创建 root(与注释和部分文档预期相反),易误导安全预期、造成"无可用管理员"或反之。 |
| 中 | internal/config/config.go:39 |
JWT 密钥来源不可见:Spec.SecretKey = env.Runtime.JwtSecretKey,yaml 中的 SecretKey: CHANGE_ME 被覆盖忽略。真实密钥强度/是否强制设置取决于 bsm-sdk 与环境变量,本模块无法确认【信息不足】。 |
| 低 | internal/logic/hello/ping.go:25-35 + internal/routers/register.go:32 |
/rest/mgt/session 为匿名调试端点:写入固定 session 并打印到 stdout,属调试残留,可被探测/泄露。 |
| 低 | internal/logic/pub/login.go:108-116 |
SaveToken 将 token 写入 Redis 时 TTL=0(永不过期);该函数未被任何路径调用(死代码),但一旦启用即永久缓存 token。 |
管理员鉴定本身正确:
routers/register.go:42-43在管理路由挂RequireAdmin,其IsSuperAdmin(internal/middleware/rbac.go:19-36)按id回查 DB 确认账号为root或拥有超级管理员角色,不以 JWT 声明直接判定,模块内无明显绕过。唯一残留风险在于 SDK 的ParseAuth是否校验 JWT 签名(见 6.4)。
6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
|---|---|---|
| 中 | internal/logic/tool/tool.go:5-14 |
列表查询无分页上限:GetSizeAndPage 对 size<=0 才取默认 10,否则直接使用请求值。所有 *Fetch/list 接口(user/role/app/pmn/dpt)接受任意 size,可触发无上限批量查询(全表扫描 / DoS / 数据拖库)。 |
| 中 | internal/config/config.go:24-29 |
InitRootUserEnabled 在 nil 时返回 false,与同文件注释及 README 宣称的"默认 true"不一致(同 6.1 中)。 |
| 中 | internal/models/impl.go:106,113 |
SQL Debug 全量开启:mysql/postgres 连接均硬编码 Debug: true,所有 SQL(含敏感查询)进入日志,生产环境应关闭。 |
| 低 | internal/logic/user/create.go:57-72、internal/logic/application/create.go:50-68 |
创建时对已软删的同 phone/account(或 workspace/title)记录执行 Unscoped().Delete 硬删除再重建,可能误删历史数据且不可恢复。 |
6.3 未完成实现
internal/logic/pub/forget.go:103-104:ForgetPwdByPwd(凭旧密码改密)为空 stub,且未注册路由。internal/logic/pub/login.go:108:SaveToken死代码(无调用点)。internal/models/init_menu.go:16引用etc/menu.json;本仓库etc/下为menu.json(已存在)但scripts/other提及menu_structure.json,文件缺失会导致InitRootUserData事务因Load失败而回滚、root 创建失败【信息不足,需确认部署包含该文件】。- 全模块无 Go 单元测试覆盖逻辑层(仅
libs校验;test/下多为.http手工用例)。
6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
|---|---|---|
| 中 | internal/models/impl.go:106,113 |
SQL Debug 全量日志(同 6.2),泄露查询结构。 |
| 低 | internal/logic/user/del.go:16-78 |
无"禁止删除 root / 超级管理员"保护:超级管理员可删除 root 或另一超级管理员,导致管理面锁死(DoS)。 |
| 低 | internal/logic/pub/login.go:80、user/create.go:46 等多处 |
大量 printer.Error/Info 打印账号、手机号、内部错误等敏感信息,存在信息泄露。 |
| 低 | 全模块 | 逻辑层无单元测试;config 注释与实现、README 与真实路由前缀多处不一致,增加维护与误配风险。 |
| 【信息不足】 | git.apinb.com/bsm-sdk/core/middleware.ParseAuth |
管理员判定的前提是 JWT 签名被 ParseAuth 正确校验;该函数在 bsm-sdk 内,本模块不可见,无法确认签名校验是否到位(若未校验,可伪造 id/account=root 的 token 越权)。建议核实 SDK 实现。 |
7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
|---|---|---|---|
| M1 | 高 | AppKey 登录明文比对密码列(哈希即凭证) |
密码哈希泄露即等于账号失陷 |
| M2 | 高 | dev 默认 root/123456 弱管理员账号 |
非生产环境被注入后门凭据 |
| M3 | 中 | InitRootUser 注释/实现/文档行为不一致 |
安全预期误导、root 可能未创建 |
| M4 | 中 | JWT 密钥来源不可见 | 密钥强度不确定 |
| M5 | 中 | 列表查询无分页上限 | 全表扫描 / DoS / 拖库 |
| M6 | 中 | SQL Debug 全量日志 | 查询与数据泄露 |
| M7 | 低 | 匿名 session 调试端点 | 探测/信息泄露 |
| M8 | 低 | 可删除 root、无单测、信息打印 | 健壮性与可维护性 |
| M9 | 【信息不足】 | SDK ParseAuth 签名校验未知 |
管理员越权潜在风险 |
8. 修复建议(务实项)
- 移除 AppKey 明文比对:
checkAppKeyAndStatus(pub/login.go:147)不应以userPwd != pwd直接比较密码列。应用密钥登录应走独立的应用密钥表/字段校验,而非复用用户登录密码;至少应与checkPwdAndStatus同样走 bcrypt/MD5 哈希比对,杜绝"哈希即凭证"。 - 收敛默认账号:dev 之外不要启用
InitRootUser;若需启用,生成随机初始口令并强制首次登录改密,移除硬编码123456(init_db.go:16)。 - 统一配置语义:
config.go注释与InitRootUserEnabled实现保持一致(明确"省略即不初始化"或"省略即初始化"),并同步修正 README,避免误导。 - 分页上限:
tool.GetSizeAndPage(tool/tool.go)对size设上限(如 ≤200),所有*Fetch/list复用。 - 关闭 SQL Debug:
models/impl.go的Debug: true改为按环境(非 dev 关闭)。 - 删除调试端点与死代码:移除
/rest/mgt/session(register.go:32)与SaveToken(login.go:108)。 - 删除保护:
user/del.go增加"禁止删除 root / 超级管理员"校验。 - 确认 JWT 签名校验:核实 bsm-sdk
ParseAuth是否校验签名;如未校验,管理员鉴定即形同虚设——此为前提性修复。
本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。