240 lines
30 KiB
Markdown
240 lines
30 KiB
Markdown
# 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. 服务定位与职责
|
||
|
||
面向客户端启动阶段的基础数据服务,提供三类能力:
|
||
|
||
1. **配置下发**(`Check.Config`):按 `app + os` 返回配置键值列表,支持 `os = ALL` 的全平台兜底。
|
||
2. **版本更新检查**(`Check.Updates`):按 `app + os + arch` 取"最新"一条版本记录,与客户端上报版本比对,返回更新包信息。
|
||
3. **基础字典**(`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 接口无多余方法。
|
||
|
||
> 鉴权补充(两处问题叠加):
|
||
> 1. **模块 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`)。
|
||
> 2. **独立部署时模块网关自身不带任何鉴权**:`Start` 把 `GatewayMux` 直接交给 `http.ListenAndServe`(`service.go:106-111`、`:120-129`),没有包 JWT 中间件;JWT 校验由聚合入口 `pkgs/all/internal/server/authorization.go:42-66` 提供。因此独立部署下这 6 个接口对任何网络可达方**完全开放**(含下面 6.1 的 SQL 注入)。
|
||
> 3. **聚合部署时这 6 个接口都需要 JWT,与模块 yaml 的匿名声明矛盾**:`pkgs/all/etc/default_dev.yaml:24-43` 的 `Authorization.Anonymous` 不含任何 `/initial.*`。
|
||
> 4. **即便把 `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) | 均 `not null` |
|
||
| `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. 核心流程
|
||
|
||
```mermaid
|
||
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.go` 5 个错误码全部未使用**:`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. 修复建议(务实项)
|
||
|
||
1. **修 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` 现有的参数校验处,无需新增层。
|
||
2. **修版本比较**:在 `internal/logic/check/updates.go:39` 之前解析版本(仓库已有 `github.com/coreos/go-semver` 作为间接依赖,见 `go.mod:26`;也可直接按 `.` 分段比较,避免新增直接依赖),仅当 `服务端版本 > 客户端版本` 时返回更新;并对 `in.Version` 做非空+格式校验。同时为 `initial_apps` 的 `(app, os, arch, version)` 加唯一索引,避免同版本重复记录。
|
||
3. **修 `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` 改为按模型字段结构化查询并显式注明外部依赖。
|
||
4. **恢复错误处理**:`country_cache.go:19-24` 与 `areas_cache.go:33-38` 参照 `datas_cache.go:22-25` 的写法,先判断 `Find` 的 `err` 再回写缓存,避免把空结果写入 Redis。
|
||
5. **鉴权口径对齐(二选一,必须选)**:若这 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` 也不会生效。
|
||
6. **生产配置**:`etc/initial_prod.yaml:4-10` 改成真实生产库/Redis(或明确该文件在聚合部署下不生效,避免误用)。
|
||
7. **配置校验**:`internal/config/config.go:36` 的 `conf.NotNil` 补上 `Databases`;或在 `impl.NewImpl` 前显式判空并打印缺失项名。
|
||
8. **查询边界**:`datas_cache.go:22` 增加明确的条件与 `LIMIT`;`Areas` 增加返回条数上限(如 5000)或按 `deep` 分层返回。
|
||
9. **去重逻辑**:`internal/logic/check/config.go:34-40` 改为显式优先级比较(先比 `Os == 请求os`,再比 `id` 更大),删掉冗余的 `else if` 分支。
|
||
10. **清理与补测**:删除 `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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。
|