# 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` 读 `/etc/_.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["httpMiddleware:canonicalHTTPPath + isAnonymous
authorization.go:55-66"] A -->|"匿名 或 JWT 有效"| G["grpc-gateway mux ServeHTTP(响应写入 bufferedResponse)
server.go:63-65"] A -->|"鉴权失败"| E1["HTTP 200 + {code,message,details,timeseq}
authorization.go:122-126"] G -->|"status != 404"| F1["flush 缓冲响应给客户端
server.go:66-68"] G -->|"status == 404"| H["gin ServeHTTP 兜底
server.go:70"] H --> R1["POST /rpc/:module/:service/:method
server.go:61"] R1 --> D["resolveMethod:gRPC reflection 取 descriptor(带缓存)
dynamic.go:115-176"] D --> D1["streaming → 直接报 Unimplemented
dynamic.go:79-82"] D --> D2["protojson 解 JSON(≤4 MiB)→ 内网 gRPC Invoke(insecure)
dynamic.go:84-112"] H --> R2["/rest/fts/... /rest/logs/... /rest/mgt/...(各模块自注册)
service/{fts,logs,mgt}.go → module 内层 JwtAuth"] H -->|"无路由"| N["gin 默认 404(纯文本)"] D2 --> E2["内网 gRPC 再次经过 unary 拦截器校验 JWT
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` 读 `/etc/default_.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` 拼进 metadata(gateway `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 404(gateway `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-65535(SDK `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"一类改造不在此列。