14 KiB
14 KiB
passport(用户账号与身份认证)代码审计报告
| 项 | 内容 |
|---|---|
| 审计对象 | module/base/passport |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件约 44 个(含 cmd/、internal/、service/、proto/ 6 个);pb/ 下约 21 个生成 .go 仅看签名 |
| 入口 | cmd/main(gRPC + grpc-gateway 单进程)、cmd/cli(调试入口)、聚合入口 pkgs/all、pkgs/ecmall |
| 对外协议 | gRPC(passport.Account/Login/Register/Forget/Verify)+ REST(grpc-gateway,路径形如 POST /passport.Login/Pwd) |
| 结论摘要 | 认证核心存在致命缺陷:验证码登录/注册/找回均不校验验证码;找回密码仅凭 identity 即可改密且以未加盐 MD5 存储;密码哈希使用 bcrypt.MinCost(cost=4);KYC 回调为空 stub;Token 缓存键前缀大小写不一致。 |
1. 服务定位与职责
用户通行证(账号、密码、第三方登录、KYC 认证、标签、统计)的统一身份服务。负责账号注册、密码/验证码登录、密码找回、KYC 验证回调、用户资料与标签管理。它是认证与账户存储服务,不负责网关鉴权本身(鉴权依赖上层网关注入 metadata,由 service.ParseMetaCtx 读取)。
2. 代码结构与入口
| 路径 | 职责 |
|---|---|
cmd/main/main.go |
独立进程入口:配置→初始化 impl→建 gRPC/Gateway→server.New(nil)→service.New 启动 |
cmd/cli/main.go |
调试入口:仅打印数据库配置 |
internal/config/config.go |
配置结构(Base/Databases/MicroService/Gateway/Token/Kyc/WeChat 等)与校验;设置 JWT 过期 |
internal/impl/impl.go |
初始化 Redis、DB、Etcd、内存缓存实例 |
internal/vars/{token,status,provider}.go |
常量:TokenPrefix="/TOKEN/"、账号状态、第三方 provider |
internal/excode/ex.go |
空文件,无内容 |
internal/models/* |
11 个模型 + query.go(InitData/Exists) + cache.go(Redis 缓存与验证码接口) |
internal/logic/common/token.go |
用 env.Runtime.JwtSecretKey 签发 JWT |
internal/logic/{login,register,forget,account,verify}/* |
各业务方法实现 |
internal/server/* |
gRPC server 注册(5 个 service 全部委托 logic) |
service/{expose,dependencies}.go |
聚合宿主注入接口(供 pkgs/all 复用共享连接) |
proto/*.proto |
5 个服务契约(无 google.api.http 注解,REST 路径由网关生成) |
依赖注入:service.Dependencies 支持外部传入 Redis/Etcd/DB/Cache,非 nil 时覆盖 internal/impl 对象,供聚合入口复用共享连接。
3. 接口清单
proto 中未声明
ping/hello服务,但etc/*.yaml的MicroService.Anonymous列出passport.ping.hello,与当前模块实现不一致(疑似残留)。
| 方法 | 路径(gRPC / REST) | 功能 | 鉴权 | 实现位置 |
|---|---|---|---|---|
Login.Pwd |
passport.Login/Pwd |
账号+密码登录 | 否(公开) | logic/login/pwd.go |
Login.Code |
passport.Login/Code |
验证码登录 | 否(公开) | logic/login/code.go |
Login.Quick |
passport.Login/Quick |
验证码快捷登录并注册 | 否(公开) | logic/login/quick.go |
Register.Pwd |
passport.Register/Pwd |
账号密码注册 | 否(公开) | logic/register/{pwd,do}.go |
Register.Code |
passport.Register/Code |
手机验证码注册 | 否(公开) | logic/register/{code,do}.go |
Forget.Verify |
passport.Forget/Verify |
验证手机号和验证码 | 否(公开) | logic/forget/verify.go |
Forget.Reset |
passport.Forget/Reset |
重置密码 | 否(公开) | logic/forget/reset.go |
Account.Get |
passport.Account/Get |
获取完整用户信息 | 是(ParseMetaCtx) |
logic/account/get.go |
Account.SetData |
passport.Account/SetData |
更新用户资料 | 是 | logic/account/set_data.go |
Account.SetPassword |
passport.Account/SetPassword |
修改密码 | 是 | logic/account/set_password.go |
Account.TagCreate |
passport.Account/TagCreate |
新增标签 | 是 | logic/account/tag_create.go |
Account.TagRemove |
passport.Account/TagRemove |
删除标签 | 是 | logic/account/tag_remove.go |
Account.Statistics |
passport.Account/Statistics |
获取统计数据 | 是 | logic/account/statistics.go |
Verify.Request |
passport.Verify/Request |
发起 KYC 验证 | 是 | logic/verify/request.go |
Verify.JumioCallback |
passport.Verify/JumioCallback |
KYC 回调 | 否(公开) | logic/verify/jumio_callback.go |
全部声明的 gRPC 方法均已委托到 logic(server 层无 Unimplemented 未实现项);但
Verify.JumioCallback与Forget.Verify为逻辑占位/伪实现(见 6.3)。
4. 数据模型与表
| 表名 | 字段 | 类型 | 键/约束 | 说明 |
|---|---|---|---|---|
passport_account |
id |
uint | PK | 自增主键 |
identity |
varchar | - | 用户唯一码(对外公开) | |
account |
varchar(255) | 默认 '' |
账号 | |
phone |
varchar(20) | 默认 '' |
手机号 | |
email |
varchar(255) | 默认 '' |
邮箱 | |
password |
varchar(255) | not null | 密码(bcrypt;重置时误写为 MD5) | |
salt |
varchar(255) | not null | 密码盐 | |
passport_data |
passport_id/passport_identity |
- | - | 关联账号 |
nickname/avatar/sign/cover |
varchar | 默认 '' |
资料 | |
sex/birthday |
int8/time | - | 性别/生日 | |
country/province/city/area |
varchar | 默认 '' |
地区 | |
score/level |
int32 | 默认 0 | 积分/等级 | |
rights |
varchar(255) | 默认 '' |
权限 | |
email_verify/phone_verify/face_verify/document_verify/kyc_verify |
int32 | 默认 0 | 各验证状态 | |
passport_tags |
passport_id/name/icon |
- | - | 用户标签 |
passport_verify |
passport_id + 各验证时间/证件/KYC 字段 |
- | - | 认证详情(KYC 回调未写入) |
passport_provider |
provider/provider_id/access_token/refresh_token |
- | - | 第三方登录绑定 |
passport_score / passport_statistics / passport_rights_expiry / passport_notify |
见各模型文件 | - | - | 积分/统计/特权有效期/消息 |
5. 核心流程
flowchart TD
A["POST passport.Login/Code"] --> B["校验 phone/code/country 非空"]
B --> C["GetPassportAccountByField(phone)"]
C --> D["直接签发 JWT 返回"]
D --> E["未比对 Redis 验证码"]
F["POST passport.Forget/Reset"] --> G["校验 identity/password 非空"]
G --> H["Update password = Md5(pwd)"]
H --> I["账号被接管/密码无法登录"]
J["POST passport.Login/Pwd"] --> K["bcrypt.CompareHashAndPassword"]
K --> L["签发 JWT 并写 Redis(/TOKEN/)"]
M["POST passport.Register/Pwd"] --> N["bcrypt.MinCost 哈希+盐"]
N --> O["Create account + data"]
6. 审计发现
6.1 安全
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/login/code.go:16-59 |
验证码登录不校验验证码。仅判断 in.Phone/in.Code/in.Country 非空后直接 GetPassportAccountByField("phone",...) 并签发 JWT;从未调用 GetVerificationCodeCache 比对 in.Code。任意已知手机号 + 任意 code 即可登录。 |
| 高 | internal/logic/login/quick.go:20-64 |
快捷登录并注册同样不校验 code,且当手机号不存在时(:32-64)自动创建账号。攻击者可用任意 code 对任意手机号批量建号并登录。 |
| 高 | internal/logic/forget/reset.go:17-41 |
找回密码无验证码/令牌校验。仅凭 identity + password 即 Update("password", ...)。前置 Forget.Verify 也未真正校验 code(见 6.3)。identity 在登录/注册响应中公开返回,攻击者可据此直接接管任意账号。 |
| 高 | internal/logic/forget/reset.go:31 |
以未加盐 MD5 存储密码:Update("password", utils.Md5(in.Password)),与全模块 bcrypt 体系不一致;且登录校验走 bcrypt(login/pwd.go:27),重置后的密码既不安全又无法用于登录(逻辑自相矛盾)。 |
| 中 | internal/logic/register/do.go:30、internal/logic/account/set_password.go:35、internal/models/query.go:21 |
密码哈希 cost 过低:使用 bcrypt.MinCost(=4)而非默认 10,抗暴力破解能力弱。 |
| 中 | internal/vars/token.go:3 vs internal/logic/register/do.go:84 |
Token 缓存键前缀不一致:vars.TokenPrefix="/TOKEN/" 用于 login/pwd.go:48、login/code.go:41;而 register/do.go:84 用 config.Spec.Token.Prefix(yaml 为 /token/)。同一 identity 在 Redis 中形成两把不同键,登出/校验可能取错或失效。 |
| 低 | internal/logic/login/code.go:41 |
写 Redis token 时 TTL 传 0(永不过期),与 login/pwd.go:48 用 _vars.JwtExpire 不一致;缓存 token 永久留存(仅依赖 JWT 自身过期)。 |
| 低 | test/grpc/main.go:45 |
硬编码测试 JWT(authorization 明文 token,含 id/identity)出现在测试代码,若被复用可越权。 |
6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
|---|---|---|
| 中 | internal/logic/account/get.go:44 |
impl.DBService.Where("passport_id=?", AUTH.ID).First(data) 未处理 error;查询失败或记录不存在时 data 仍是零值指针(非 nil),if data != nil 恒真,后续 data.Birthday.Format 等执行,DB 错误被吞掉、返回空资料。 |
| 中 | internal/logic/forget/reset.go:31 |
重置密码不同步更新 salt(passport_account.salt 列存在),且写入 MD5,与 bcrypt 校验路径不兼容(同 6.1)。 |
| 中 | internal/logic/register/do.go:53,70 |
账号与扩展数据两次 Create 非事务:若第二步失败,会留下无扩展数据的孤儿账号。 |
| 低 | internal/logic/register/code.go:13-31 |
Code 注册仅校验手机号格式与唯一性,未校验 in.Code,与"验证码注册"语义不符(同 6.1 高)。 |
6.3 未完成实现
internal/logic/verify/jumio_callback.go:13-35:KYC 回调仅printer.Info打印完整 payload 后返回成功,未校验回调签名、未更新passport_verify的 KYC 状态——典型占位实现。internal/logic/forget/verify.go:14-32:名义"验证手机号和验证码",实际不比对 code(无任何 Redis 验证码查询),属伪实现。internal/models/query.go:12-39:InitData计划初始化demo/welcome默认账号(密码 bcrypt MinCost),但cmd/main/main.go:37已注释未启用;含硬编码默认口令,属潜在隐患(一旦启用即成后门)。internal/models/cache.go:144-172:IncrementLoginAttempts/ClearLoginAttempts从未被调用,README 声称的"内置限流"未落地。internal/models/cache.go:124-142:SetVerificationCodeCache/GetVerificationCodeCache提供验证码存取,但所有登录/注册/找回逻辑均未调用,验证码形同虚设(与 6.1 高关联)。internal/excode/ex.go:空文件,错误码未定义。cmd/cli/main.go:仅打印 DB 配置,无实际用途(调试残留)。etc/*.yaml的SecretKey: CHANGE_ME字段在config.go的SrvConfig无对应字段,未被解析;真实 JWT 密钥来自env.Runtime.JwtSecretKey(logic/common/token.go:10),其初始化不在本模块可见范围【信息不足】。- 全模块无
*_test.go单元测试。
6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
|---|---|---|
| 中 | internal/logic/account/get.go:44 |
DB 错误未处理(同 6.2)。 |
| 中 | internal/logic/login/*、forget/reset.go、verify/jumio_callback.go |
多处 printer.Error/Info 输出内部错误与敏感参数(如 jumio_callback.go:21 打印完整 KYC 回调含 PII、reset.go:33 打印 identity、login/pwd.go:34 打印 DB 错误),存在信息泄露。 |
| 低 | internal/vars/token.go:3 vs config |
Token 前缀两套定义(见 6.1)。 |
| 低 | 全模块 | 无单元测试;InitData/CreateAccount 等存在死代码或未被调用路径。 |
7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
|---|---|---|---|
| P1 | 高 | 验证码登录不校验 code | 任意手机号可登录,认证失效 |
| P2 | 高 | 快捷登录/注册不校验 code 且自动建号 | 批量伪造账号 |
| P3 | 高 | 找回密码仅凭 identity 改密、无校验 | 任意账号被接管 |
| P4 | 高 | 重置密码以未加盐 MD5 存储且与 bcrypt 不兼容 | 密码泄露风险 + 重置后无法登录 |
| P5 | 中 | bcrypt.MinCost(cost=4) | 密码暴力破解成本降低 |
| P6 | 中 | Token 缓存键前缀大小写不一致 | 登录态缓存错乱/登出失效 |
| P7 | 中 | KYC 回调空 stub、Forget.Verify 伪实现 | 认证状态不可信 |
| P8 | 中 | 验证码缓存接口从未被调用 | 全链路验证码机制失效 |
| P9 | 低 | 内部错误/PII 经日志泄露 | 信息泄露 |
| P10 | 低 | DB 错误未处理、无事务、无测试 | 健壮性与可维护性 |
8. 修复建议(务实项)
- 验证码必须校验:在
login/code.go、login/quick.go、register/code.go、forget/verify.go中调用GetVerificationCodeCache(phone)并比对in.Code(一次一消);forget/reset.go必须要求先通过Verify且校验一次性令牌,再允许改密。 - 重置密码用 bcrypt:
forget/reset.go:31改为bcrypt.GenerateFromPassword([]byte(newPwd+salt), bcrypt.DefaultCost)并同步更新salt,与登录校验一致。 - 提高哈希 cost:
register/do.go、account/set_password.go、query.go的bcrypt.MinCost改为bcrypt.DefaultCost(10)。 - 统一 Token 前缀:仅保留一处定义(建议用
config.Spec.Token.Prefix),login与register共用,避免/TOKEN/与/token/双键。 - KYC 回调落地:
jumio_callback.go增加签名校验并更新passport_verify的kyc_verify/kyc_status;Forget.Verify真正比对验证码。 - 收敛默认账号:保持
InitData不启用,或删除该函数与硬编码welcome口令。 - 错误处理:
account/get.go:44处理First的 error;register/do.go两次 Create 用事务包裹。 - 清理:移除
cmd/cli调试输出、空excode/ex.go、未调用的限流函数(或真正接入);敏感信息勿进printer。
本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。