30 KiB
initial(客户端初始化与基础字典)代码审计报告
| 项 | 内容 |
|---|---|
| 审计对象 | module/base/initial |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件 27 个、1066 行(含 test/grpc/main.go 226 行非 _test.go 的 main 程序、internal/models/query.go 1 行空文件);proto 3 个(check.proto 67 行、data.proto 78 行、const.proto 313 行);pb/ 生成文件按 check/data/const 各 3~4 个 |
| 入口 | cmd/main(gRPC + HTTP Gateway 单进程);聚合入口 pkgs/all(pkgs/all/internal/service/initial.go:9-20) |
| 对外协议 | gRPC + REST Gateway,REST 路径 /initial.Check/{Hello,Config,Updates}、/initial.Data/{Country,Areas,Datas} |
| 结论摘要 | 6 个只读接口共 1066 行代码,无写接口。版本更新判断用字符串不等比较(check/updates.go:39),客户端版本比服务端新时会"升级"到旧版本(强制降级),且无任何语义版本/白名单(平台/架构/最低版本)约束;check/config_cache.go:21 把请求参数 os 直接拼接进 ORDER BY,构成 SQL 注入;data/areas_cache.go:33-35 查询了模型上根本不存在的 enabled/show_town/sort_order 三列,Areas 接口在仓库给定的表结构下必然查库失败;所有 6 个接口在模块 yaml 中被登记为匿名(etc/initial_dev.yaml:15-21),而聚合部署白名单(pkgs/all/etc/default_dev.yaml:24-43)未放行其中任何一条,且模块 yaml 的匿名条目写法与框架实际匹配格式不一致,两边都落空。 |
1. 服务定位与职责
面向客户端启动阶段的基础数据服务,提供三类能力:
- 配置下发(
Check.Config):按app + os返回配置键值列表,支持os = ALL的全平台兜底。 - 版本更新检查(
Check.Updates):按app + os + arch取"最新"一条版本记录,与客户端上报版本比对,返回更新包信息。 - 基础字典(
Data.Country/Data.Areas/Data.Datas):国家列表、行政区划(国家→省→市→区县,可选乡镇)、系统标签字典。
不承担字典的增删改,不负责安装包分发(只返回 files 字段),Check.Hello 为健康检查。
2. 代码结构与入口
| 路径 | 职责 |
|---|---|
cmd/main/main.go |
独立进程入口。ServiceKey = "Initial"(:15,大写);config.New → impl.NewImpl → server.New(nil)(:25)→ service.New(...) + srv.Start()(:27-43) |
cmd/cli/main.go |
11 行占位,仅 log.Println("Hello Cli")(:8-9) |
internal/config/config.go |
SrvConfig(Base/Databases/MicroService/Rpc/Gateway/APM/Etcd);New 做端口/IP 校验、conf.NotNil(Spec.Service, Spec.Cache)(:36) |
internal/impl/impl.go |
初始化 MemorySerice/RedisService/DBService/EtcdService(:20-29);MemorySerice 拼写错误(:16,22) |
internal/logic/check/hello.go |
Hello 健康检查,只回显 code(:12-32),无依赖探活 |
internal/logic/check/config.go |
Config:缓存取配置 + 内存去重 + 转 pb(:13-55) |
internal/logic/check/config_cache.go |
GetConfigByCache:Redis → DB(app + (os OR ALL))→ 回写缓存(:10-29) |
internal/logic/check/updates.go |
Updates:取最新一条 initial_apps 记录并比较版本(:14-55) |
internal/logic/data/country.go / country_cache.go |
Country 查询 + 缓存(enabled = true,sort_order ASC, name ASC) |
internal/logic/data/areas.go / areas_cache.go |
Areas 查询 + 缓存,默认国家 1(中国)(areas.go:15-17) |
internal/logic/data/datas.go / datas_cache.go |
Datas 查询 + 缓存,并在结果前拼接一条硬编码"全部"项(datas.go:23-31) |
internal/models/*.go |
5 个表模型 + database.AppendMigrate 注册;query.go:1 只有 package models(空文件) |
internal/server/{new,check_server,data_server}.go |
注册 Check/Data 到 gRPC;独立运行时开 reflection(new.go:36-38) |
internal/excode/ex.go |
5 个"参数错误"错误码,全模块无引用(见 6.3) |
internal/routers |
不存在(走 gRPC + HTTP Gateway) |
service/{expose,dependencies}.go |
聚合宿主注入点:覆盖 Redis/Etcd/DB/Cache(dependencies.go:19-32) |
proto/{check,data,const}.proto |
前两个定义本模块 6 个 rpc;const.proto 为本模块无关的共享消息 |
etc/{initial_dev,initial_prod,initial_test}.yaml |
三份内容一致(Port 12101 / Gateway 12102 / 同一 127.0.0.1 库与 Redis) |
test/grpc/main.go |
package main 的手工调用脚本(非 _test.go),默认打 api.apinb.com:10020(:18) |
test/http/*.http |
2 个手工请求样例 |
3. 接口清单
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
|---|---|---|---|---|
| POST | /initial.Check/Hello |
健康检查,回显 code |
无(模块 yaml 登记匿名 etc/initial_dev.yaml:16) |
internal/logic/check/hello.go:12 ← internal/server/check_server.go:19 |
| POST | /initial.Check/Config |
按 app + os 下发配置 |
无(etc/initial_dev.yaml:17) |
internal/logic/check/config.go:13 ← check_server.go:24 |
| POST | /initial.Check/Updates |
版本更新检查 | 无(etc/initial_dev.yaml:18) |
internal/logic/check/updates.go:14 ← check_server.go:29 |
| POST | /initial.Data/Country |
启用国家列表 | 无(etc/initial_dev.yaml:19) |
internal/logic/data/country.go:11 ← data_server.go:18 |
| POST | /initial.Data/Areas |
行政区划(默认中国、默认市级) | 无(etc/initial_dev.yaml:20) |
internal/logic/data/areas.go:13 ← data_server.go:23 |
| POST | /initial.Data/Datas |
系统标签字典(含硬编码"全部"项) | 无(etc/initial_dev.yaml:21) |
internal/logic/data/datas.go:12 ← data_server.go:28 |
| POST | gRPC /initial.Check/*、/initial.Data/* |
同上(同一 handler) | 同上 | internal/server/new.go:33-34 |
声明但未实现/占位的接口:无。proto/check.proto:8-20 声明 3 个 rpc、proto/data.proto:7-16 声明 3 个 rpc,与 internal/server/check_server.go:19-31、internal/server/data_server.go:18-30 逐一对应;pb/*_grpc.pb.go 的 server 接口无多余方法。
鉴权补充(两处问题叠加):
- 模块 yaml 的匿名清单不会生效:
etc/initial_dev.yaml:14为MicroService.Enable: false,而匿名清单只在Enable == true时才写入 etcd(D:\work\bsm-sdk\core\service\service.go:66-90,register.SetAnonymous在:86)。- 独立部署时模块网关自身不带任何鉴权:
Start把GatewayMux直接交给http.ListenAndServe(service.go:106-111、:120-129),没有包 JWT 中间件;JWT 校验由聚合入口pkgs/all/internal/server/authorization.go:42-66提供。因此独立部署下这 6 个接口对任何网络可达方完全开放(含下面 6.1 的 SQL 注入)。- 聚合部署时这 6 个接口都需要 JWT,与模块 yaml 的匿名声明矛盾:
pkgs/all/etc/default_dev.yaml:24-43的Authorization.Anonymous不含任何/initial.*。- 即便把
Enable改为true,清单写法也对不上:框架发现的方法名是服务名.方法名(service.go:149-158,如Check.Config、Data.Areas),HTTP 网关的规范化路径是/initial.Check/Config(authorization.go:105-112),而配置里写的是带前缀的initial.Check.Config(etc/initial_dev.yaml:17)——归一化后为/initial.Check.Config,与上述两种格式都不相等,永远不会命中匿名判断。
4. 数据模型与表
表 initial_apps(internal/models/initial_apps.go:11-20,GORM 自动迁移)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
id/created_at/updated_at/deleted_at |
uint / timestamp | PK / 软删 | 来自 types.Std_IICUDS |
version |
varchar(255) | default '' | 版本号,字符串(:13) |
app / os / arch |
varchar(255) | default '' | 应用名 / 操作系统 / 架构 |
summary |
text | default '' | 更新说明 |
files |
text | default '' | 更新文件与 hash |
pubdate |
timestamp | — | 发布时间(:19) |
无
enabled/status字段,也无(app, os, arch)唯一索引 → 同一三元组可堆积任意多条记录,靠ORDER BY pubdate DESC, id DESC LIMIT 1取"最新"(updates.go:23-25)。
表 initial_config(internal/models/initial_config.go:9-16)
| 字段 | 类型 | 说明 |
|---|---|---|
app / os |
varchar(255) | os = 'ALL' 表示全平台(:12) |
key / value |
text | 配置键值(:13-14),key 为 text 且无索引 |
version |
int64 | 版本号(:15) |
表 initial_country(internal/models/initial_country.go:6-21)
| 字段 | 类型 | 说明 |
|---|---|---|
iso2 / iso3 |
varchar(255) | 均声明 uniqueIndex(:8-9) |
num_code / phone_code |
varchar(255) | ISO 数字码 / 国际区号 |
currency / currency_symbol / region / name / local_name |
varchar(24 | 255) |
timezones / translations |
text(无 tag) | 时区 / 翻译 |
enabled |
bool | default true(:19),查询条件列 |
sort_order |
int32 | default 0(:20),排序列 |
表 initial_areas(internal/models/initial_areas.go:7-18)
| 字段 | 类型 | 说明 |
|---|---|---|
id / country_id / country_code / pid / deep |
uint / varchar / int32 | 层级关系 |
name / pinyin_prefix / pinyin / ext_id / ext_name |
varchar | 名称与拼音 |
注意:该模型没有
enabled、sort_order、show_town三个字段,而areas_cache.go:33-35的查询却使用了它们,详见 6.2 第 1 条。
表 initial_datas(internal/models/initial_datas.go:10-17)
| 字段 | 类型 | 说明 |
|---|---|---|
data_type |
varchar(255) | 按类型分组,有索引 |
key |
varchar(255) | uniqueIndex(:13) |
title / remark / icon |
varchar | 展示字段 |
5. 核心流程
flowchart TD
A["客户端请求 /initial.Check/Updates"] --> B["校验 app/os/arch/version 非空,否则 ErrInvalidArgument"]
B --> C["SELECT * FROM initial_apps WHERE app=? AND os=? AND arch=? ORDER BY pubdate DESC, id DESC LIMIT 1"]
C --> D{"记录是否存在"}
D -->|"不存在"| E["返回 Identity=0, Version=0(视为无更新)"]
D -->|"查询出错"| F["返回 errcode.ErrDB"]
D -->|"存在"| G{"in.Version != data.Version(字符串不等)"}
G -->|"不相等"| H["返回新版本 identity/version/summary/files/pubdate"]
G -->|"相等"| I["返回 identity/version,不带 summary/files"]
J["客户端请求 /initial.Data/Areas"] --> K["入参为空则默认 CountryId=1(中国)"]
K --> L["Redis Get BuildKey(areas, 条件, show_town)"]
L -->|"命中"| M["直接返回"]
L -->|"未命中"| N["SELECT * FROM initial_areas WHERE enabled=true AND country_id=? AND show_town=? ORDER BY sort_order ASC, name ASC"]
N --> O["回写 Redis(忽略 DB 错误)并返回"]
6. 审计发现
6.1 安全
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/check/config_cache.go:21 |
SQL 注入。Order("CASE WHEN os = '" + os + "' THEN 0 ELSE 1 END, id DESC") 把请求参数 os(in.Os,来自 check/config.go:15-17 的必填校验,只判空、无字符过滤)直接字符串拼接进 ORDER BY 子句。GORM 的 Order(string) 按原始 SQL 下发,不经占位符转义,因此 os 传入形如 1' THEN 0 ELSE 1 END) UNION SELECT ... -- 的载荷即可越权读取/破坏数据。同一条查询的 Where 用的是 ? 占位符(config_cache.go:20),唯独 Order 走了拼接,属明显疏漏。 |
| 高 | etc/initial_dev.yaml:14-21、etc/initial_prod.yaml:14-21、etc/initial_test.yaml:14-21 |
6 个接口全部登记为匿名。叠加 6.1 上一行的 SQL 注入,攻击面为"未鉴权的任意请求"。另见第 3 节鉴权补充:该清单在 Enable: false 下不会发布,且条目格式(initial.Check.Config)与框架实际匹配格式(Check.Config / /initial.Check/Config)不一致。 |
| 中 | pkgs/all/etc/default_dev.yaml:24-43 |
聚合部署白名单未放行任何 /initial.* 路径 → 与模块 yaml 的匿名声明直接矛盾,表现为"本地/单模块可达,聚合后 401"。 |
| 中 | internal/logic/check/config.go:15-17 |
入参仅做非空校验,app/os 无长度上限、无字符白名单,随后被用作缓存键(config_cache.go:13)与 SQL 条件;os 还进入 Order 拼接(见上)。 |
| 低 | internal/logic/data/areas_cache.go:22-23 |
country_id 作为查询条件未校验其存在性与归属,任意整数都直接落到 WHERE country_id = ?;excode.ErrInvalidCountry(internal/excode/ex.go:11)定义了却未使用。 |
| 低 | internal/logic/check/hello.go:14-21 |
Hello 把请求方传入的 code 原样回显进 Details("Hello " + in.Code),无长度限制;虽为健康检查,仍是反射型输出的最小隐患。 |
6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/data/areas_cache.go:33-35 |
查询使用了 enabled、show_town、sort_order 三列,但 models.InitialAreas(initial_areas.go:7-18)没有这三个字段,database.AppendMigrate(initial_areas.go:20-22)也不会创建它们。按仓库内证据,该 SQL 在任何库上都会因"列不存在"报错;而错误在第 37 行被 RedisService.Set 的返回值覆盖,于是接口不报错、而是把空结果写进缓存并返回空列表 30 分钟(vars.DefaultTTL = 30m,D:\work\bsm-sdk\core\vars\cache.go:16)。若线上表结构由仓库外脚本额外加了这三列(README.md:279 提到的 scripts/initial_areas_202509081640.sql 在仓库中不存在),则可运行——但仓库内无此依据,【信息不足:数据库实际列结构未随仓库提供】。 |
| 高 | internal/logic/check/updates.go:39 |
版本比较用字符串不等:if in.Version != data.Version。没有语义版本解析、没有"仅当服务端更新时才提示升级"的判断。后果:①客户端版本高于表内最新记录(如客户端 2.0.0、表内 1.9.0)时,接口仍返回 1.9.0 并带 files,等于向已升级的客户端下发旧安装包(强制降级);②任何书写差异(v1.0.0 vs 1.0.0、1.0.0 vs 1.0.00)都会触发一次"更新";③updates.go:23-25 只按 pubdate DESC, id DESC 取一条,pubdate 相同时按 id 倒序,若运营误插入一条 pubdate 为未来的记录,会长期占据"最新"。 |
| 中 | internal/logic/check/config.go:34-40 |
去重逻辑与排序意图相反。SQL 用 ORDER BY CASE WHEN os = '<os>' THEN 0 ELSE 1 END, id DESC(config_cache.go:21)让"专有 OS 优先、且 id 大者优先";但去重条件 if existing, exists := configMap[item.Key]; !exists || item.Os != "ALL" 会让后遍历到的非 ALL 记录覆盖先前记录,而遍历顺序正是 SQL 的顺序(大 id → 小 id),于是同一 key 存在多条同 OS 记录时,最终保留的往往是 id 最小的那条,与"取最新"相反。else if existing.Os == "ALL" && item.Os != "ALL" 分支与首条件重复,只在 existing.Os == "ALL" 时也已被首条件命中,属冗余。 |
| 中 | internal/logic/data/country_cache.go:19-24、areas_cache.go:33-38 |
DB 错误被吞掉。两者的 Find(&x).Error 结果都在下一行被 RedisService.Set(...) 的返回值覆盖,函数最终返回的是 Set 的错误;查询失败(表不存在、连接超时)时接口不报错,而是把 nil 切片缓存 30 分钟,形成"空数据缓存穿透式污染"。datas_cache.go:22-25 是唯一做了错误判断的实现(err != gorm.ErrRecordNotFound 则返回 ErrDB),三处风格不一致。 |
| 中 | internal/logic/data/datas_cache.go:22 |
Order("data_type ASC, id ASC") 的 Find 无任何过滤条件(连 enabled 都没有),全表返回且无 LIMIT;initial_datas 也没有启用/停用字段可用。字典表一旦膨胀,每次缓存失效都会全量扫表并把全量结果写入 Redis。 |
| 中 | internal/logic/data/areas_cache.go:22-23、areas.go:15-17 |
Areas 无分页、无层级/数量上限:show_town = true 时会把国家下全部乡镇一次性返回(Where("show_town = ?", in.ShowTown)),响应体无界。默认 CountryId=1(中国)也是硬编码常量,若字典中 id=1 不是中国则语义错位。 |
| 低 | internal/logic/check/updates.go:28-34 |
未找到记录时返回 Identity: "0", Version: "0" 并带 nil error,与"查库失败"(:35 返回 ErrDB)语义不同,但客户端难以区分"无该应用"与"该应用无更新";Identity/Version 被赋固定字符串 "0",不是空串,容易与真实版本号混淆。 |
| 低 | internal/logic/check/updates.go:20-25 |
查询未使用 ctx(:14 的入参 ctx 全程未使用),上游超时/取消不会传导到 DB,慢查询无超时控制。country.go:11、areas.go:13、datas.go:12、config.go:13、hello.go:12 同样是 ctx 未使用。 |
| 低 | internal/logic/data/datas.go:23-31 |
业务字典里硬编码了中文常量"全部"/"default"/"all"(DataType: "default", Key: "all"),既与库内数据的 data_type 取值可能冲突(initial_datas.key 还有 uniqueIndex,见 initial_datas.go:13),也让多语言/多端差异化无法实现。 |
| 低 | internal/logic/check/config_cache.go:27-28 |
缓存设置失败时 return configs, err 把 Redis 错误当成整体失败返回;上层 config.go:27-30 随即转成 ErrDB("数据库错误"),误导排障方向。 |
| 低 | internal/logic/data/areas_cache.go:26 |
缓存键由原始 SQL 片段拼成:BuildKey("areas", "country_id = ?", val, in.ShowTown) → 形如 bsm:areas:country_id = ?:<val>:<bool>(BuildKey 实现见 D:\work\bsm-sdk\core\cache\redis\cache.go:19-26,直接 fmt.Sprintf(":%v"))。键里带空格与 ?,可读性差;一旦哪天改 key 的写法,历史缓存键即全部失效。 |
6.3 未完成实现
internal/excode/ex.go5 个错误码全部未使用:ErrInvalidApp/ErrInvalidOS/ErrInvalidArch/ErrInvalidVersion/ErrInvalidCountry(:7-11)在本模块无任何引用点(全模块检索仅命中该文件自身),实际实现用的是 SDK 的errcode.ErrInvalidArgument/ErrDB(如check/config.go:16,29)。整个internal/excode包是死代码。internal/models/query.go是 1 行空文件(只有package models,无任何声明)。internal/config/config.go:19的Rpc字段全模块无使用点;etc/initial_*.yaml:31-34的Rpc段整体被注释 → 死配置。internal/impl/impl.go:16,22,24的MemorySerice/RedisService:MemorySerice仅被service/dependencies.go:30赋值,本模块内无任何读取;RedisService有实际使用(各*_cache.go)。即内存缓存层是初始化了但不用的预留。Check.Hello未做任何依赖探活:check/hello.go:23-24自身注释写着"可以在这里添加更多的健康检查逻辑,例如检查数据库连接、Redis连接等",当前恒返回Code: 0→ 健康检查无法反映真实可用性。proto/const.proto:1-313定义了OrderSummaryItem、FeedPostItem、GroupPostItem、RelationItem、MarketLoginReply、CmsCategoryItem等大量与本模块无关的消息,本模块未引用任何一条 → 死定义,且被pb/const.pb.go编译进本模块。test/grpc/main.go(package main) 是手工调用脚本而非测试:不在_test.go文件中,go test不会执行;默认地址硬编码api.apinb.com:10020(:18),且调用成功后只打印条数,无断言。cmd/cli/main.go:1-10仅打印两行日志。- 全模块无任何
*_test.go,internal/logic/**与internal/models/**零测试覆盖。
6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
|---|---|---|
| 中 | internal/config/config.go:36 |
conf.NotNil(Spec.Service, Spec.Cache) 未校验 Databases。with.Databases(D:\work\bsm-sdk\core\with\databases.go:13-15)在 Source 为空时直接 panic("No Database Source Found !"),因此缺 Databases 段时 impl.NewImpl()(impl.go:26)在启动阶段 panic,且报错不含配置项名。 |
| 中 | etc/initial_prod.yaml:7,10(及 :1-41 全篇) |
生产配置是开发配置的副本:Databases.Source 指向 host=127.0.0.1 ... dbname=rst_dev(:7),Cache 指向 redis://null:...@127.0.0.1:6379/(:10),与 initial_test.yaml、initial_dev.yaml 三份完全一致。生产若直接使用该文件,会连本机库/本机 Redis。 |
| 中 | pkgs/all/internal/service/initial.go:9-20 |
聚合入口调用 moduleService.Expose 时未传 Config,而 internal/service/expose.go:12-16 的 ExposeOptions 也确实没有该字段(与同仓 sender 的 expose.go:13-20 不一致)。因此聚合部署下 initial 的配置完全取自 pkgs/all 的共享 DB/Redis;本模块 etc/*.yaml 在聚合场景下是不被读取的,容易被误认为生效。 |
| 低 | internal/impl/impl.go:16,22 |
变量名拼写错误 MemorySerice(应为 MemoryService)。同 workspace 的 pkgs/all/internal/impl 用的是正确拼写(见 pkgs/all/internal/service/initial.go:15 的 impl.MemoryService),跨模块阅读易误判为两个变量。 |
| 低 | internal/config/config.go:20、initial_prod.yaml:14 |
SrvConfig.Rpc 字段与 yaml 段位均无使用方(见 6.3)。 |
| 低 | README.md:322-329 |
README 的缓存策略表写着"配置数据 10 分钟 / 国家数据 24 小时 / 地区数据 12 小时 / 系统数据 6 小时",但四处实现统一使用 vars.DefaultTTL(config_cache.go:27、country_cache.go:23、areas_cache.go:37、datas_cache.go:27),实际值是 30 分钟(D:\work\bsm-sdk\core\vars\cache.go:16)。文档与实现不一致。 |
| 低 | README.md:180-187 |
README 把 6 个接口标为 POST /initial.Check/Hello 等,与 authorization.go:105-112 的规范化路径一致,这部分是对的;但 README.md:342,346 声明的 /health、/metrics 端点在本模块无任何注册 → 不实描述。 |
| 低 | README.md:76-101 |
README 的目录树宣称存在 cache/、swagger/、scripts/、Dockerfile、docker-compose.yml、Makefile,这些在本模块均不存在(实际目录只有 cmd/internal/pb/proto/etc/test/service 与三个 md)。 |
| 低 | etc/supervisor.bsm-apps-initial.conf:6 |
进程以 user=root 运行。 |
| 低 | internal/logic/data/areas_cache.go:33 |
Where("enabled = ?", true) 用原始 SQL 字符串表达布尔条件(而非 Where(&models.InitialAreas{Enabled: true}) 这类结构化条件),正是这类写法让 6.2 第 1 条的"列不存在"缺陷在编译期无法被发现。 |
7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
|---|---|---|---|
| I1 | 高 | config_cache.go:21 用字符串拼接把 os 注入 ORDER BY |
未授权读取/篡改任意表、库结构泄露 |
| I2 | 高 | updates.go:39 用字符串不等判断版本,未判断"服务端是否更新" |
客户端降级到旧版本、版本书写差异导致误报更新 |
| I3 | 高 | areas_cache.go:33-35 查询模型上不存在的 3 列,且 DB 错误被 Set 覆盖 |
Areas 接口必然返回空列表并缓存 30 分钟,数据不可用且难排查 |
| I4 | 高 | 6 个接口全部匿名(模块 yaml),独立部署网关无鉴权中间件 | 未鉴权访问全部初始化数据面,放大 I1 |
| I5 | 中 | 模块匿名清单与聚合白名单互相矛盾,且条目格式与框架匹配规则不一致 | 上线 401 / 误判防护已生效 |
| I6 | 中 | config.go:34-40 去重与 ORDER BY 意图相反,同 OS 多条时保留 id 最小者 |
下发的配置值可能不是最新配置 |
| I7 | 中 | country_cache.go/areas_cache.go 吞掉 DB 错误并缓存空结果 |
空数据被缓存 30 分钟,故障期数据面静默降级 |
| I8 | 中 | datas_cache.go:22 全表无过滤、无分页;Areas 无条数上限 |
大表全量扫描 + 响应体膨胀 |
| I9 | 中 | config.go:36 未校验 Databases,缺失即启动 panic;initial_prod.yaml 仍是本机地址 |
部署可用性、生产误连本机库/Redis |
| I10 | 低 | 死代码/死配置(internal/excode、models/query.go、Rpc 段、const.proto、cmd/cli、MemorySerice)+ 零 _test.go |
可维护性、无法回归验证 |
8. 修复建议(务实项)
- 修 SQL 注入(最高优先):把
internal/logic/check/config_cache.go:21的Order从拼接改为白名单映射,例如按os是否等于请求值在 Go 侧决定排序,或改用clause.Expr+占位符;同时给os/app加长度与字符白名单(只允许[A-Za-z0-9_.-],≤32)。校验位置就在internal/logic/check/config.go:15-17现有的参数校验处,无需新增层。 - 修版本比较:在
internal/logic/check/updates.go:39之前解析版本(仓库已有github.com/coreos/go-semver作为间接依赖,见go.mod:26;也可直接按.分段比较,避免新增直接依赖),仅当服务端版本 > 客户端版本时返回更新;并对in.Version做非空+格式校验。同时为initial_apps的(app, os, arch, version)加唯一索引,避免同版本重复记录。 - 修
Areas列不匹配:在internal/models/initial_areas.go:7-18补上Enabled bool、SortOrder int32、ShowTown bool(并在仓库内补上对应的建表/迁移 SQL,README.md:279引用的scripts/initial_areas_202509081640.sql应随仓库提供);若这三列本就该由外部脚本维护,则在areas_cache.go改为按模型字段结构化查询并显式注明外部依赖。 - 恢复错误处理:
country_cache.go:19-24与areas_cache.go:33-38参照datas_cache.go:22-25的写法,先判断Find的err再回写缓存,避免把空结果写入 Redis。 - 鉴权口径对齐(二选一,必须选):若这 6 个接口应当匿名,把
/initial.Check/Hello、/initial.Check/Config、/initial.Check/Updates、/initial.Data/Country、/initial.Data/Areas、/initial.Data/Datas按authorization.go:105-112的规范化格式加入pkgs/all/etc/default_dev.yaml:24-43(并同步生产配置);若应当鉴权,则从etc/initial_{dev,test,prod}.yaml:15-21删除匿名条目。注意模块 yaml 的initial.Check.Config写法与框架匹配规则不符,改Enable也不会生效。 - 生产配置:
etc/initial_prod.yaml:4-10改成真实生产库/Redis(或明确该文件在聚合部署下不生效,避免误用)。 - 配置校验:
internal/config/config.go:36的conf.NotNil补上Databases;或在impl.NewImpl前显式判空并打印缺失项名。 - 查询边界:
datas_cache.go:22增加明确的条件与LIMIT;Areas增加返回条数上限(如 5000)或按deep分层返回。 - 去重逻辑:
internal/logic/check/config.go:34-40改为显式优先级比较(先比Os == 请求os,再比id更大),删掉冗余的else if分支。 - 清理与补测:删除
internal/excode(或真正在参数校验处使用其中错误码)、internal/models/query.go、proto/const.proto中与本模块无关的消息、cmd/cli占位;变量名MemorySerice→MemoryService;至少为Updates(同版本/高版本/低版本/无记录)与Config(专有 OS 覆盖 ALL、同 OS 多条取最新)各补 2~3 条表驱动用例。
本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。
9. 整改记录(2026-09-22)
本节记录按本报告结论执行的代码整改。整改遵循最小修正原则:未引入新框架、抽象层、DTO/VO、事件总线,未拆分服务边界,未修改任何
proto/*.proto与生成的pb/*.go;新增/修改注释均为中文;口令类摘要统一使用 bcrypt(验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:GOWORK=off go build ./...+GOWORK=off go vet ./...+gofmt -l(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import,属本机既有问题)。
| 编号 | 级别 | 问题 | 处理结果 |
|---|---|---|---|
| I1 | 高 | config_cache.go 把 os 参数字符串拼进 ORDER BY(SQL 注入) |
已修复:改为排序白名单校验,绝不拼接用户输入 |
| I2 | 高 | updates.go 用字符串不等判断版本 |
已修复:改为按 . 分段转数字的语义版本比较,客户端不再被误判降级 |
| I3 | 高 | areas_cache.go 查询模型上不存在的 3 列且 DB 错误被 Set 覆盖 |
已修复:按模型真实列名修正;错误必检查,失败不写缓存 |
| I4 | 高 | 6 个接口全部匿名,独立部署网关无鉴权中间件 | 已修复:收敛匿名白名单,仅保留确需公开的接口 |
未纳入本轮范围
报告中「中」「低」级别的项(分页上限、死代码、README 与实现不符、单测缺失、可维护性等)本轮未处理;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
本轮整改未修改任何
proto/*.proto与pb/*.go,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。