Files
full/docs/initial.md
2026-09-22 21:15:34 +08:00

240 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 GatewayREST 路径 `/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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。