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

216 lines
34 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.
# pkgs/all全量聚合宿主代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `pkgs/all`module 路径 `bsm/full/pkgs/all`,全量聚合宿主) |
| 所属域 | 聚合运行入口(组合基础与平台 10 个 + 电商交易 4 个 + 资金支付 1 个,共 15 个服务) |
| 审计日期 | 2026-09-22 |
| 代码规模 | Go 文件 27 个 / 1331 行(其中 `_test.go` 4 个 / 238 行);`etc/default_dev.yaml` 145 行;`go.mod` 148 行 |
| 监听端口 | gRPC `0.0.0.0:12000`、HTTP `0.0.0.0:12001``pkgs/all/etc/default_dev.yaml:3-8` |
| 结论摘要 | 分层清晰、分流实现可读,但**上线路径缺失**(无 prod 配置、构建脚本不覆盖聚合入口)、**两个密钥默认值可直接启动**JWT 签名密钥与 session 密钥均为占位串)且入口不做校验、**grpc-gateway 的 404 与业务 NotFound 无法区分**导致真实错误响应被 Gin 的 404 覆盖、**gRPC reflection 无开关且匿名可达**、**全链路无 TLS**、**进程级客户端 IP 可被 X-Forwarded-For 伪造**。另有匿名白名单与模块自身声明口径不一致、缺注册表测试护栏等一致性问题。 |
## 1. 定位与职责
`pkgs/all` 是"全量聚合宿主":在**一个进程**内启动 15 个业务模块的 gRPC 服务、grpc-gateway 路由和原生 REST 路由进程内注入一份共享的数据库、Redis、etcd 与内存缓存,并统一管理监听地址、鉴权和优雅关闭。
它**不承载任何业务逻辑**:所有 `internal/service/*.go` 只做 `ExposeOptions` 组装与注入(`pkgs/all/internal/service/ads.go:9-20` 等 15 个文件,每个 14-22 行),路由与落库全部在 `module/` 各模块内。它也不负责建表与种子数据:聚合路径从不调用模块的 `impl.NewImpl()`,只调用模块 `service.applyDependencies``module/ec/order/service/dependencies.go:19-29`)。
`pkgs/ecmall` 的关系:两者是同一份代码的**两份拷贝**`internal/server` 下 4 个文件逐字节相同(见 6.3-C5注册表仅差 `cloud` 一项。
## 2. 代码结构与关键文件
| 路径 | 行数 | 职责 |
| --- | --- | --- |
| `cmd/main/main.go` | 58 | 进程入口:`--workspace` 解析 → `config.New``impl.NewImpl``server.New``service.Expose` → 注册信号 → `srv.Start` |
| `internal/config/config.go` | 124 | 聚合配置结构(`Server`/`Authorization`/`Databases`/`Services`/5 个模块专项节)、监听地址规范化与校验、共享配置回填、`Enabled` 服务开关 |
| `internal/server/server.go` | 133 | gRPC server挂 unary 鉴权拦截器 + reflection、grpc-gateway mux、gin engine、`/rpc/:module/:service/:method` 动态路由、404 兜底分流、`*http.Server` 参数、`Stop` |
| `internal/server/authorization.go` | 126 | JWT 签发校验HS256、HTTP 中间件、gRPC unary 拦截器、匿名白名单匹配、`/rpc/...``/{pkg}.{Svc}/{M}` 路径规范化 |
| `internal/server/dynamic.go` | 194 | 动态 RPC通过 gRPC reflection 取 descriptor → `dynamicpb` + `protojson` → 内网 gRPC `Invoke`方法描述符缓存、4 MiB 请求体上限、头部白名单透传 |
| `internal/server/response.go` | 78 | 统一错误体 `{code,message,details,timeseq}`、gRPC code → SDK errcode 映射 |
| `internal/impl/impl.go` | 42 | 进程级共享资源Memory、Redis、DB、Etcd |
| `internal/service/service.go` | 46 | 注册表15 项)+ session 中间件 + 按 `Enabled` 逐个 `Expose` |
| `internal/service/{ads,cloud,cms,feedback,fts,initial,logs,mgt,passport,sender,address,mall,market,order,wallet}.go` | 14-22 | 各模块注入:`Dependencies` + `GRPC`/`Gateway``Engine`+(可选 `Config` |
| `etc/default_dev.yaml` | 145 | 唯一一份配置端口、DB/Redis DSN、`SecretKey``Authorization`(含匿名白名单)、`Services`、5 个模块专项节 |
| `internal/server/authorization_test.go` | 102 | 匿名/裸 JWT/`Bearer` 拒绝、iat 超期拒绝 |
| `internal/server/dynamic_test.go` | 76 | 动态 RPC 调 `grpc.health.v1.Health/Check`、头部透传过滤 |
| `internal/server/server_test.go` | 24 | gin 路由可用性 |
| `internal/config/config_test.go` | 36 | dev 配置非空 + gRPC/HTTP 端口不同 + 密钥长度 + 5 个专项节存在 |
| `go.mod` | 148 | 15 个 module replace 到 `../../module/...``bsm-sdk/core` replace 到 `../../../../bsm-sdk/core` |
注册表(`internal/service/service.go:14-33`15 项,顺序即注册顺序):`ads, cloud, cms, feedback, fts, initial, logs, mgt, passport, sender, address, mall, market, order, wallet`。与 `etc/default_dev.yaml:46-61``Services` 列表逐项一致(已核对)。
## 3. 启动流程与请求分流
启动顺序(`cmd/main/main.go:22-57`
1. `flag` 解析 `--workspace`(默认 `default`),经 `utils.MustString``^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$` 校验并转小写SDK `utils/ext.go:39-46`)。
2. `config.New(ServiceKey)``conf.New``<Prefix>/etc/<key>_<mode>.yaml` → 归一化两个监听地址 → 校验两地址不同、`Authorization.Key` 非空且长度 ∈ {16,24,32}、`Expire>0` → 写 `env.JwtSecretKey``coreVars.JwtExpire` → 回填共享配置(`pkgs/all/internal/config/config.go:56-83`)。
3. `impl.NewImpl()`Memory → Redis → DB → Etcd`internal/impl/impl.go:30-42`)。注意 DB 缺失时 SDK 直接 `panic`(见 6.3-C3
4. `allserver.New(...)`:建 gRPC server**无条件** `reflection.Register`+ gin engine`gin.New()` + Logger + Recovery
5. `service.Expose(srv)`:先挂 session 中间件,再按注册表 + `Enabled` 逐个注册;任一模块 `Expose` 出错则整体 `panic``internal/service/service.go:35-46``cmd/main/main.go:42-44`)。
6. `srv.Start(grpcAddr, httpAddr)`:先监听两个 TCP再建动态网关失败则关闭两个 listener`/rpc/:module/:service/:method`,组装 404 兜底 handler起两个 goroutine 并阻塞在第一个返回值上。
请求分流(`internal/server/server.go:61-78` + `authorization.go:96-120`
```mermaid
flowchart TD
C["客户端请求"] --> A["httpMiddlewarecanonicalHTTPPath + isAnonymous<br/>authorization.go:55-66"]
A -->|"匿名 或 JWT 有效"| G["grpc-gateway mux ServeHTTP响应写入 bufferedResponse<br/>server.go:63-65"]
A -->|"鉴权失败"| E1["HTTP 200 + {code,message,details,timeseq}<br/>authorization.go:122-126"]
G -->|"status != 404"| F1["flush 缓冲响应给客户端<br/>server.go:66-68"]
G -->|"status == 404"| H["gin ServeHTTP 兜底<br/>server.go:70"]
H --> R1["POST /rpc/:module/:service/:method<br/>server.go:61"]
R1 --> D["resolveMethodgRPC reflection 取 descriptor带缓存<br/>dynamic.go:115-176"]
D --> D1["streaming → 直接报 Unimplemented<br/>dynamic.go:79-82"]
D --> D2["protojson 解 JSON≤4 MiB→ 内网 gRPC Invokeinsecure<br/>dynamic.go:84-112"]
H --> R2["/rest/fts/... /rest/logs/... /rest/mgt/...(各模块自注册)<br/>service/{fts,logs,mgt}.go → module 内层 JwtAuth"]
H -->|"无路由"| N["gin 默认 404纯文本"]
D2 --> E2["内网 gRPC 再次经过 unary 拦截器校验 JWT<br/>authorization.go:42-53"]
```
四条入口的能力边界:
| 入口 | 形态 | 覆盖范围 | 鉴权节点 |
| --- | --- | --- | --- |
| 原生 gRPC | `/{pkg}.{Svc}/{M}` | 12 个 gRPC 模块fts/logs/mgt 无 gRPC 面) | gRPC unary 拦截器(`server.go:32` |
| grpc-gateway | `POST /{pkg}.{Svc}/{M}` | 同 12 个模块 | HTTP 中间件 + 内网 unary 拦截器(双重) |
| 动态 RPC | `POST /rpc/{pkg}/{Svc}/{M}` | 同 12 个模块,仅 unary | HTTP 中间件(按网关形态匹配白名单)+ 内网 unary 拦截器 |
| 原生 REST | `/rest/{fts,logs,mgt}/...` | 仅这 3 个模块 | HTTP 中间件 + 模块内层 `JwtAuth`(仅 `/rest/fts/uploader``/rest/mgt/{user,app,role,pmn,dpt}/*` |
## 4. 配置与安全
| 项 | 现状 | 证据 |
| --- | --- | --- |
| 监听地址 | gRPC `12000` / HTTP `12001``BindIP 0.0.0.0`;两地址相同则 panic | `etc/default_dev.yaml:3-8``config.go:60-62` |
| 端口缺省 | `Port` 为空时**随机分配 1024-65535**,且"两地址不同"校验必然通过 | SDK `conf/new.go:87-97``config.go:85-89` |
| JWT | HS256裸 JWT 放 `Authorization` 头(无 `Bearer`);校验算法、`exp``iat` 不为未来、`now-iat ≤ Expire` | `authorization.go:68-94``authorization_test.go:41` |
| JWT 密钥 | `Authorization.Key` 必须 16/24/32 字节;写入全局 `env.JwtSecretKey`,模块签发处共用 | `config.go:63-73` |
| 默认密钥 | `CHANGE_ME_32_BYTE_JWT_SECRET_KEY`(恰好 32 字节 → 通过校验) | `etc/default_dev.yaml:22``config.go:66-69` |
| session 密钥 | `config.Spec.SecretKey`= `conf.Base.SecretKey`)拼 `"-session"` 作为 cookie store 的 HMAC key**无任何校验** | `internal/service/service.go:36`SDK `conf/types.go:11``config.go:63-72` 未校验 |
| 默认 session 密钥 | `CHANGE_ME` | `etc/default_dev.yaml:17` |
| 鉴权中间件覆盖 | HTTP单一中间件包住"gateway + gin"整个 handler因此 `/rpc/...``/rest/...` 全部在覆盖内gRPC`grpc.UnaryInterceptor`(无 Stream | `server.go:32,74``authorization.go:55-66` |
| 匿名白名单 | 11 个 gRPC 网关形态 + 8 个 REST 路径,共 19 条;`/grpc.reflection.``/grpc.health.` 前缀**硬编码放行** | `etc/default_dev.yaml:24-43``authorization.go:96-103` |
| gRPC reflection | 无条件注册、无开关;且被 `isAnonymous` 放行为匿名 | `server.go:33``authorization.go:98-100` |
| TLS | 全模块无 TLS`net.Listen("tcp", ...)`、内网 `insecure.NewCredentials()``grep -i "tls\|certificate\|x509" pkgs` 无命中 | `server.go:45,49``dynamic.go:45` |
| HTTP server 参数 | `ReadHeaderTimeout=10s``IdleTimeout=120s``MaxHeaderBytes=1MiB`、**无 `ReadTimeout`/`WriteTimeout`** | `server.go:72-78` |
| gRPC server 参数 | 无 MaxRecvMsgSize、无 keepalive、无 StreamInterceptor | `server.go:32` |
| 客户端 IP | `gin.New()``SetTrustedProxies`gin v1.12.0 默认信任 `0.0.0.0/0``::/0` | `server.go:34`gin `gin.go:225,214` |
| 限流 | 入口层无任何限流/防爆破逻辑 | `grep -i "ratelimit\|limiter\|throttle" pkgs` 无命中 |
| 白名单一致性 | 与模块自身声明不一致logs 三条 REST、ads.Fetch/ByPos | 见 6.1-A7 |
| 服务开关 | `BSM_SERVICES`(非空即覆盖 `Services` `Services`;名字大小写不敏感、含 `all` 即全开 | `config.go:109-124` |
| 运行模式 | gin 未按 `BSM_RuntimeMode` 设置模式 | `server.go:34`SDK `middleware/mode.go:9-16` |
| CORS | 聚合未启用;独立 fts/mgt 启用SDK 实现为 `AllowAllOrigins: true` | `server.go:34``module/base/fts/cmd/main/main.go:26``module/base/mgt/cmd/main/main.go:39`SDK `middleware/cors.go:8-18` |
匿名白名单逐条核对结果11 条 gRPC 条目全部对应真实方法(`passport``Login.{Pwd,Code,Quick}``Register.{Pwd,Code}``Forget.{Verify,Reset}``Verify.{Request,JumioCallback}``module/base/passport/proto/*.proto``mall.Staff/Login``module/ec/mall/proto/staff.proto:9``market.Agency/Login``module/ec/market/proto/agency.proto:9`8 条 REST 条目也全部对应已注册路由(`module/base/fts/internal/routers/register.go:22-23``module/base/logs/internal/routers/register.go:24``module/base/mgt/internal/routers/register.go:31-36`)。即**无死条目**;问题在"该放行的没放行"(见 6.1-A7
## 5. 审计发现
### 5.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `etc/default_dev.yaml:17` + `internal/service/service.go:36` | **session 密钥用未校验的占位值**`srv.HTTP.Use(sessions.Sessions("mysession", cookie.NewStore([]byte(config.Spec.SecretKey+"-session"))))``SecretKey` 取自 `conf.Base`SDK `conf/types.go:11`),配置值为 `CHANGE_ME``config.New` 只校验 `Authorization.Key``config.go:63-72`),从不校验 `SecretKey`。默认配置下 cookie 的 HMAC 密钥是公开可猜字符串,`mysession` 可被伪造(当前唯一使用点是 `module/base/mgt/internal/logic/hello/ping.go:25-35` 的 SessionDemo 演示)。 |
| **高** | `etc/default_dev.yaml:22` + `internal/config/config.go:66-69` | **JWT 签名密钥默认值是可用的公开字面量**`CHANGE_ME_32_BYTE_JWT_SECRET_KEY` 恰好 32 字节,直接通过长度校验并写入 `env.NewEnv().JwtSecretKey``config.go:73`)。任何知道该字面量的人都能自行 HS256 签发带 `iat/exp` 的 token通过 `authorization.go:74-79` 的校验。模块的 token 也由同一密钥签发(`module/base/mgt/internal/logic/pub/login.go:90``module/base/passport/internal/logic/common/token.go:10`),无法通过"只改某一侧"缓解。 |
| **高** | `pkgs/all/etc`(目录仅 1 个文件) | **不存在生产配置**`conf.New``<Prefix>/etc/default_<mode>.yaml`SDK `conf/new.go:35-36``BSM_RuntimeMode` 默认 `dev`SDK `env/env.go:20`);设为 `prod` 时读 `default_prod.yaml`,缺失即回退 `workspace_default_prod.yaml``new.go:39-43`),两者都不存在 → `log.Fatalf``new.go:49-52`)。即 `BSM_RuntimeMode=prod` 的聚合进程**无法启动**,只能以 dev 模式上线(偏离 README 的"评估关闭 reflection、入口层启用 TLS"前提)。 |
| **中** | `internal/server/server.go:33` + `authorization.go:96-100` | **gRPC reflection 无开关且匿名可达**`reflection.Register(grpcServer)` 无条件执行,`isAnonymous``/grpc.reflection.` 前缀直接返回 true因此匿名用户可拉取全部已注册服务的 `FileDescriptorProto`,枚举 35 个服务/方法名与完整消息结构(`dynamic.go:124-143` 本身就走这条路径)。这不泄露数据,但等于把内部 API 全貌公开,便于后续针对性构造调用。 |
| **中** | `internal/server/authorization.go:98` | `/grpc.health.` 被硬编码为匿名,但全仓库没有任何模块注册 health 服务(`grep -rn "health" module` 仅命中注释与 `wallet/logic/payment/hello.go` 的字符串)。当前是死名单;一旦按惯例补上 health 服务,它会先天地对匿名开放。 |
| **中** | `internal/server/server.go:45,49` + `dynamic.go:45` | **全链路明文**。两个 listener 都是 `net.Listen("tcp", ...)`,无 `TLSConfig`;动态网关到本机 gRPC 用 `insecure.NewCredentials()``pkgs``grep -i "tls\|certificate\|x509"` 无任何命中。JWT 与业务数据以明文在 `0.0.0.0:12000/12001` 上传输,反向代理是否终止 TLS 属于部署前提,进程内无任何能力。 |
| **中** | `internal/server/server.go:34``dynamic.go:180-188` | **进程级客户端 IP 可被伪造**。聚合用 `gin.New()` 且未调用 `SetTrustedProxies`gin v1.12.0 默认 `trustedProxies = {"0.0.0.0/0","::/0"}``ForwardedByClientIP=true`gin `gin.go:225,214`),因此 `c.ClientIP()` 直接采用客户端自报的 `X-Forwarded-For`;动态 RPC 又把请求里所有 `x-` 前缀头原样透传给内网 gRPC`dynamic.go:182-188`grpc-gateway 也会把请求 `X-Forwarded-For` 拼进 metadatagateway `runtime/context.go:185-193`)。受影响落点示例:登录 token 里记录的 client IP`module/base/mgt/internal/logic/pub/login.go:90`)。 |
| **中** | `internal/server/authorization.go:55-66``server.go:35` | **匿名登录入口无任何限流**。白名单里的 `passport.Login/Pwd|Code|Quick``passport.Register/*``passport.Forget/*``market.Agency/Login``mall.Staff/Login``/rest/mgt/login` 均可无限次调用;入口层只有 `gin.Logger()` 与 JWT 校验,没有失败计数、锁定或速率限制(`grep -i "ratelimit\|limiter\|throttle" pkgs` 无命中)。 |
| **中** | `etc/default_dev.yaml:24-43` 对比 `module/base/logs/internal/routers/register.go:18-29``module/base/ads/etc/ads_dev.yaml:15-16` | **匿名口径不一致(双向)**。① logs 的 `POST /rest/logs/create|fetch|total` 注册在模块的"匿名组"(该模块自己的 `JwtAuth` 被注释掉,`register.go:23`),而聚合白名单只列了 `/rest/logs/ping` → 同一路由独立进程匿名、聚合进程要求 JWT。② `ads.Fetch/ByPos` 在模块 dev 配置里被声明匿名,聚合白名单没有它 → 独立可调、聚合 401`docs/ads.md` 的同类结论一致)。③ 被白名单放开的 3 个登录入口passport 4 条、market/mall 各 1 条)在聚合下确实是匿名的,口径一致。 |
| **低** | `internal/server/authorization.go:96-120` | 白名单为**精确字符串匹配**,且 gRPC 网关形态与 REST 真实路径混列在同一 `[]string` 里;`/rpc/...` 只在"恰好 4 段"时才被改写成网关形态(`canonicalHTTPPath`)。新增方法/路由若忘记登记,只会静默变成"需要 JWT",没有任何一致性校验或启动期告警。 |
| **低** | `internal/server/dynamic.go:115-176` | 动态 RPC 的 descriptor 缓存键由**请求路径拼接**`moduleName + "." + serviceShortName``dynamic.go:73`),未命中时每次都向内网反射服务发起一次 RPC缓存无上限、无 TTL。任意匿名调用者可用随机符号名制造大量反射查询放大内网调用`protojson` 解析错误信息原样回显(`dynamic.go:95-98`)。 |
### 5.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/server/server.go:63-71` | **gateway 的 404 与业务 `NotFound` 无法区分,真实响应被丢弃**`recorder.status != http.StatusNotFound` 被当作"网关未命中路由"的判据;但 grpc-gateway 对业务 `NotFound` 也渲染成 HTTP 404gateway `runtime/errors.go:48-49`),与路由未命中的 404`runtime/mux.go:464,552`)状态码完全相同。因此任何 gateway 方法返回 `codes.NotFound`"记录不存在"这类高频业务分支)时,**已经生成好的 `{code,message,...}` 响应被丢弃**,请求落到 gin 后因无匹配路由,客户端最终收到 gin 的纯文本 `404 page not found`。 |
| **中** | `internal/server/server.go:82-89` | **启动期错误只处理一半**`errCh` 容量 2、只读一个值就返回当 HTTP `Serve` 先失败时gRPC listener 与 server 既不 `Stop` 也不 `Close`(反之亦然);`Start``Shutdown` 兜底。 |
| **中** | `cmd/main/main.go:48-53` + `internal/server/server.go:92-110` | **优雅关闭的超时被复用且错误被丢弃**`Stop(ctx)` 内部先用同一个 `ctx``GracefulStop`,超时后 `s.GRPC.Stop()`,随后 `s.http.Shutdown(ctx)` 使用的仍是**已经过期**的 ctx → 立即返回 `context deadline exceeded`,在途 REST 请求被直接中断;外层 `_ = srv.Stop(ctx)` 又丢弃了返回值,失败完全无声。 |
| **中** | `internal/server/server.go:112-133` | **全量缓冲响应**。所有 grpc-gateway 响应先写入 `bytes.Buffer``server.go:64`),判定不是 404 后才整体 `flush``server.go:125-132`响应体在内存中同时存在缓冲与写出两份列表类接口mall 商品、order 订单、cms 文章)在大响应/高并发下放大内存占用;`flush` 复制响应头用 `Add` 而非 `Set``server.go:128``WriteHeader` 无重复调用保护(`server.go:123`)。 |
| **中** | `internal/server/server.go:32` | **流式 RPC 无鉴权拦截**。鉴权只挂在 `grpc.UnaryInterceptor`,未注册 `StreamInterceptor``isAnonymous` 虽放行 `/grpc.reflection.`,但**任何**流式方法都不经过拦截器。当前 66 个 proto 文件、35 个服务均无 `stream` 声明(`grep -n "stream " --include=*.proto module` 无命中),因此尚无实际暴露;动态 RPC 侧已显式拒绝流式(`dynamic.go:79-82`),风险落在原生 gRPC 面。 |
| **中** | `etc/default_dev.yaml:73` + `module/base/fts/internal/logic/provider.go:71` | **配置里的下载地址没有对应路由**。聚合配置把上传站点写成 `http://127.0.0.1:12001/files`,本地存储返回的地址是 `Site + "/" + bucket + "/" + subdirpath + "/" + fileName`;但 fts 只注册了 `/rest/fts/ping|config|uploader``module/base/fts/internal/routers/register.go:11-26`),全仓库没有 `Static`/`StaticFS`/`/files` 路由(`grep -rn "Static\|/files" --include=*.go module/base/fts` 无命中)→ 上传成功后返回的 URL 在聚合 HTTP 端口上必然 404。 |
| **低** | `internal/config/config_test.go:11-36` | **配置测试只做非空校验**:校验 `Services` 非空、两端口不同、密钥长度、5 个专项节存在,但**不校验 `Services` 与注册表一致**`pkgs/all` 也缺少 ecmall 那样的 `internal/service/service_test.go`(实测 `go test ./pkgs/all/...` 输出 `internal/service [no test files]`)。改注册表(加/删服务)在 `pkgs/all` 没有测试护栏,只能靠人眼对齐 `service.go:14-33``etc/default_dev.yaml:46-61`。 |
| **低** | `internal/config/config.go:109-124` + `internal/service/service.go:35-46` | **服务列表写错会静默启动空宿主**`Enabled` 只做"名字匹配/含 all"判断,不校验名字是否存在;`Expose` 对 0 个服务注册不报错。若 `BSM_SERVICES``Services` 里全是无效名(例如把 `mall` 写成 `mail`),进程会正常监听 `12000/12001`,但除 reflection 外全部 404且没有任何启动期提示。 |
| **低** | `internal/config/config.go:85-89` | **端口漏配会随机绑定**`conf.CheckPort``Port` 为空时随机生成 1024-65535SDK `conf/new.go:90-97`);聚合有两个 listener`Port` 缺失时各自随机,"两地址必须不同"的校验必然通过 → 服务在随机端口"成功"启动,网关/探针配置全部失效,随机端口还可能恰好被占用而启动失败。 |
### 5.3 未完成/不一致
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `pkgs/all/etc/`(仅 `default_dev.yaml`+ `scripts/build-all-linux.sh:7,30` | 聚合入口**既没有 prod 配置也不被构建脚本覆盖**:构建脚本 `MODULE_ROOT="${WORKSPACE_ROOT}/module"` 且用 `find "${MODULE_ROOT}"` 收集模块,`pkgs/` 完全不在范围内,`pkgs/*` 也不会被复制 `*_prod.yaml`。结合 5.1 的"无 prod 配置",聚合入口当前只有"开发模式 + 手工 go build"这一条路径。 |
| **中** | `internal/config/config.go:78` + SDK `with/databases.go:13-15` | **`Databases` 未纳入配置校验**。`conf.NotNil(Spec.Service, Spec.Cache)` 只校验两项;`Databases` 缺失时 `impl.NewImpl()``impl.go:38`)内部 `panic("No Database Source Found !")` —— 崩溃点比配置校验更晚、信息更差;且 `impl.NewImpl()``server.New`/`Expose` 之前执行(`cmd/main/main.go:32-44`),此时 Redis/Etcd 已建连,失败回滚缺失(无 `defer Close`)。 |
| **中** | `module/base/passport/internal/logic/verify/jumio_callback.go:13-34` | **被匿名放开的第三方回调是空壳**`/passport.Verify/JumioCallback` 在聚合白名单里(`etc/default_dev.yaml:33`),实现只 `printer.Info` 后返回成功,代码内自带 TODO「In production, implement proper callback handling logic」且不校验回调签名。当前无实际危害不写库但补实现时必须先加签名校验否则任何人可直接伪造 KYC 结果。 |
| **低** | `pkgs/all` vs `pkgs/ecmall` | **注册表护栏不对等**ecmall 有 `internal/service/service_test.go`(断言各模块 gRPC/REST 面注册成功)与 `config_test.go:15``ecmallServices` 一致性断言,`pkgs/all` 两者都没有(只有 5.2 提到的非空校验)。 |
| **低** | `internal/server/{server,authorization,dynamic,response}.go` | **与 ecmall 的两份拷贝必须手工同步**diff 结果这 4 个文件逐字节相同(唯一差异是 `server.go:79-80` 的日志字面量 `all`/`ecmall`ecmall`internal/impl/impl.go` 仅 import 路径不同;`internal/config/config.go` 仅注释不同;注册表仅差一行(`pkgs/ecmall/internal/service/service.go``{"cloud", exposeCloud}`)。任何单侧修改都会让两个入口的鉴权/分流行为产生差异,目前没有测试或 CI 阻止这种漂移。 |
| **低** | 无 | 聚合入口**没有进程级健康检查端点**。独立入口都注册了 `app.HEAD("/", infra.Health)``module/base/fts/cmd/main/main.go:36``module/base/logs/cmd/main/main.go:39``module/base/mgt/cmd/main/main.go:41`),聚合只有三个模块自带的 `/rest/*/ping``HEAD /` 会落到 gin 404。 |
### 5.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **中** | `internal/server/server.go:72-78` | 未设置 `WriteTimeout`(也未设 `ReadTimeout`),只有 `ReadHeaderTimeout`/`IdleTimeout`/`MaxHeaderBytes`;慢速读取响应可长期占用连接。同时 gin 侧没有请求体大小限制(`/rest/fts/uploader` 的上限只在 fts 逻辑层),而动态 RPC 侧有 4 MiB 限制(`dynamic.go:30,84-92`)——两条入口的边界不对称。 |
| **低** | `internal/server/server.go:34` | 未按 `BSM_RuntimeMode` 设置 gin 模式(独立入口会调用 `middleware.Mode`SDK `middleware/mode.go:9-16`)→ 聚合在生产环境仍停留在 gin Debug 模式(除非外部设置 `GIN_MODE`)。 |
| **低** | `internal/server/server.go:34` | 聚合未启用 CORS而独立 fts/mgt 启用SDK 实现是 `AllowAllOrigins: true`)→ 同一接口"独立进程可被浏览器调用、聚合入口不可用"的行为差异。 |
| **低** | `internal/server/server.go:79-80` | 用 `fmt.Printf` 直接打印监听地址,绕过 SDK 的 `printer``gin.Logger()` 无条件记录全部请求(`server.go:35`),探针/大流量下噪音明显。 |
| **低** | `internal/server/server.go:112-133` | `bufferedResponse` 只实现 `Header/WriteHeader/Write`,没有 `http.Flusher`/`Hijacker`/`CloseNotifier`;当前 unary JSON 路径可用,但任何流式/大文件/SSE 型网关响应都会静默退化。 |
| **低** | `internal/server/dynamic.go:34-36` | 方法描述符缓存是 `map` + `sync.RWMutex`,无容量上限、无淘汰;键取自请求路径,长期运行下键空间取决于外部输入。 |
| **低** | `internal/server/authorization.go:26-39` | 匿名白名单在 `newAuthorization` 中经 `normalizePath` 归一化后装入 map重复项静默去重条目写错如把 `/rest/logs/ping` 写成 `rest/logs/ping`)不会被发现——`normalizePath` 会补上前导 `/` 掩盖这类笔误,但写成 `/rest/logs/ping/` 之类则会被静默视为不同路径。 |
## 6. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| A1 | 高 | 生产/测试配置缺失,构建脚本不覆盖 `pkgs/*` | 上线路径、部署可靠性 |
| A2 | 高 | session 密钥取未校验的 `SecretKey`(默认 `CHANGE_ME` | cookie 完整性与伪造 |
| A3 | 高 | JWT 密钥默认值是可直接使用的公开字面量 | 全系统身份伪造 |
| A4 | 高 | gateway 404 与业务 `NotFound` 不可区分,真实响应被覆盖 | 所有 gateway 方法的错误语义 |
| A5 | 中 | reflection 无开关且匿名可达 | 内部 API 全貌泄露 |
| A6 | 中 | 无 TLS | 传输机密性 |
| A7 | 中 | 客户端 IP 可经 XFF 伪造并透传到内网 metadata | 审计与风控数据可信性 |
| A8 | 中 | 匿名登录入口无任何限流 | 账号安全 |
| A9 | 中 | 匿名口径与模块声明不一致logs 三条 REST、ads.Fetch/ByPos | 独立/聚合行为不一致、上线 401 |
| A10 | 中 | 流式 RPC 不受鉴权拦截器覆盖(当前无流式方法) | 未来扩接口时的越权风险 |
| A11 | 中 | Fts 本地存储返回的 `/files` 地址无对应路由 | 文件下载不可用 |
| A12 | 中 | 优雅关闭超时后 HTTP 立即中断且错误被丢弃 | 关闭期请求可靠性 |
| A13 | 中 | gateway 响应全量缓冲 | 大响应下的内存放大 |
| A14 | 中 | `Databases` 未校验 + 资源初始化无失败回滚 | 启动期崩溃与连接泄漏 |
| A15 | 中 | Jumio 回调空壳(无签名校验)却已匿名放开 | 补实现后的数据伪造风险 |
| A16 | 低 | 无注册表一致性测试与告警、服务名写错静默空宿主 | 变更回归 |
| A17 | 低 | 端口漏配随机绑定、无 WriteTimeout、gin 模式/CORS 与独立入口不一致、无健康端点、两份拷贝需手工同步 | 运维与可维护性 |
## 7. 修复建议(务实项)
1. **补生产配置与构建覆盖**A1`pkgs/all/etc/` 增加 `default_prod.yaml`(至少替换全部 `CHANGE_ME`、明确 `BindIP`),并把 `scripts/build-all-linux.sh``MODULE_ROOT` 扩展到 `pkgs/`(或额外的 `PKGS_ROOT` 循环),同时补 `pkgs/all/etc/default_prod.yaml` 的拷贝分支。
2. **让占位密钥不能启动**A2、A3`internal/config/config.go` 现有的 `Authorization.Key` 校验旁,加两条最小校验——`Authorization.Key` 不得等于样例字面量、`SecretKey` 非空且不得等于 `CHANGE_ME`(或直接要求 32 字节)。这是 4 行代码级别的改动,不要引入配置加密框架。
3. **修 404 分流**A4让 gateway 用独立标记区分"路由未命中"。最小做法是自定义 `gwRuntime.ServeMux``WithRoutingErrorHandler`(或在 `bufferedResponse` 上记录"是否由 `DefaultRoutingErrorHandler` 写出"),只在 `codes.NotFound` **且** 路由未命中时才回退 gin其余情况直接 `flush`
4. **关掉或收紧 reflection**A5`Server` 增加一个 `Authorization.Reflection bool`(默认 false仅在为 true 时 `reflection.Register`;同时从 `isAnonymous` 中删除 `/grpc.health.` 前缀,等真有 health 服务时再显式加白名单。注意动态 RPC 依赖反射,关闭前需确认该入口是否保留——若保留,则应改为"仅允许 loopback 调用动态入口"。
5. **客户端 IP 与头部清理**A7`server.New``engine.SetTrustedProxies(...)` 指定真实代理网段(无代理时传 `nil` 关闭 XFF 取值),并在 `outgoingMetadata` 中**不要**透传客户端自带的 `x-forwarded-for`/`x-real-ip`(改为服务端重算)。
6. **匿名白名单口径对齐**A9二选一后同步两处——若要 logs 的 `create/fetch/total` 在聚合下也匿名,则加入 `Authorization.Anonymous`;若不打算匿名,则把 `module/base/logs/internal/routers/register.go:18-29` 的"匿名组"改名并补上模块自身 JwtAuth。`ads.Fetch/ByPos` 同理(改配置即可,零代码)。
7. **文件下载路由**A11要么在 fts 的路由注册里补上静态目录(本地存储方式),要么把 `Fts.Local.Site` 改成真实存在的下载前缀,二者取其一,避免返回不可访问的 URL。
8. **启动与关闭的收尾**A12、A14、A17`Start` 里任一 `Serve` 返回错误时关闭两个 listener 并 `Shutdown``Stop` 里给 `http.Shutdown` 单独一个未过期的短超时 ctx并把 `main.go:52` 的错误打印出来;`config.New` 增加 `Databases` 非空校验;`Port` 为空时改为**报错退出**而不是随机端口。
9. **入口限流**A8仅在 `httpMiddleware` 内对匿名路径加一个基于内存的简单计数/令牌桶(按路径 + 客户端 IP不引入外部依赖不要为它建框架。
10. **加固两份拷贝与测试护栏**A16`pkgs/all` 补一个与 `pkgs/ecmall/internal/service/service_test.go` 等价的注册表测试,并在 `internal/config/config_test.go` 中断言 `Services` 与注册表逐项一致;两份 `internal/server` 拷贝的同步问题,用"两处测试用例内容一致"来兜(不加抽取层)。
11. **补齐入口可观测与一致性**A13、A17 的其余项):`http.Server``WriteTimeout``engine.SetMode``BSM_RuntimeMode` 设置;注册 `HEAD /` 健康端点;如需 CORS 再显式启用(当前缺失本身不算缺陷,但需与独立入口行为对齐并明确决策)。
12. **流式方法的前置约束**A10`grpc.NewServer` 处补 `grpc.ChainStreamInterceptor`,与 unary 共用同一份 `validate`;若短期内不打算支持流式,也在代码中写明"新增流式方法前必须先加拦截器"。
> 本报告只列与现有实现直接相关的修复项,不引入新的分层或抽象封装。审计中"把两个聚合入口的 server 抽成公共包""给动态 RPC 引入请求 DTO 层""用服务网格解决 mTLS"一类改造不在此列。
## 8. 整改记录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属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| A1 | 高 | 生产/测试配置缺失,构建脚本不覆盖 `pkgs/*` | 已修复:新增 `etc/default_prod.yaml``etc/default_test.yaml`(字段名对齐 SDK 规范,敏感值用 `${ENV}` 占位);`scripts/build-all-linux.sh` 的扫描根从仅 `module/` 扩展为 `module/` + `pkgs/`,并去掉固定 `BSM_RuntimeMode=dev` |
| A2 | 高 | session 密钥取未校验的 `SecretKey`(默认 `CHANGE_ME` | 已修复:`internal/config` 增加非空与非公开默认值校验,缺失/为 `CHANGE_ME` 时启动失败并给出中文提示 |
| A3 | 高 | JWT 密钥默认值是可直接使用的公开字面量 | 已修复:在 `internal/config` 显式校验并拒绝 SDK 侧公开默认密钥(`Cblocksmesh2022C`),未通过环境变量提供合法密钥时启动失败(未改 SDK |
| A4 | 高 | gateway 404 与业务 `NotFound` 不可区分,真实响应被覆盖 | 已修复:改用 grpc-gateway 的 `WithRoutingErrorHandler` 标记「路由未命中」(`recorder.markRoutingMiss()`),仅在这种情况下才回退 Gin 的纯文本 404业务返回的 `NotFound``gwRuntime.DefaultRoutingErrorHandler` 原样透出 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。