# pkgs/ecmall(电商聚合宿主)代码审计报告 | 项 | 内容 | | --- | --- | | 审计对象 | `pkgs/ecmall`(module 路径 `bsm/full/pkgs/ecmall`,电商聚合宿主) | | 所属域 | 聚合运行入口(电商交易 4 + 资金支付 1 + 平台运营 6 + 基础支撑 3,共 14 个服务) | | 审计日期 | 2026-09-22 | | 代码规模 | Go 文件 27 个 / 1395 行(其中 `_test.go` 5 个 / 317 行);`etc/default_dev.yaml` 151 行;`go.mod` 146 行;另有 `AGENT.md` 776 行 | | 监听端口 | gRPC `0.0.0.0:12300`、HTTP `0.0.0.0:12301`(`pkgs/ecmall/etc/default_dev.yaml:3-8`) | | 结论摘要 | 与 `pkgs/all` 是同一实现的另一份拷贝(`internal/server` 4 个文件逐字节相同),差异仅在注册表(少 `cloud`)与端口;**多了一层注册表测试护栏**(`service_test.go`)、并用 `AGENT.md §4.5` 主动记录了匿名口径差异,是本次两个聚合入口里工程质量更好的一份。但**共性问题同样存在**:无 prod 配置与构建覆盖、两个密钥默认值可启动且不校验、gateway 404 与业务 `NotFound` 不可区分、reflection 无开关且匿名可枚举、无 TLS、进程级 XFF 可伪造、`/files` 下载地址无路由。此外**聚合模式不建表不播种**(`Mgt.InitRootUser: true` 在聚合下不生效),注册表清单在 3 处各存一份需手工同步。 | ## 1. 定位与职责 `pkgs/ecmall` 是面向电商业务的聚合宿主:在单进程内注册 **14 个**模块的 gRPC/gateway/REST 入口,共享一份数据库、Redis、etcd 与内存缓存,监听独立的 `12300`/`12301`,因此可与 `pkgs/all`(`12000`/`12001`)同时运行。 它同样不承载业务逻辑:`internal/service/*.go` 每个文件只有 14-22 行,只做 `ExposeOptions` 组装(`pkgs/ecmall/internal/service/ads.go:9-20`)。 与 `pkgs/all` 的差异(已用 `diff` 全量核对): | 维度 | `pkgs/all` | `pkgs/ecmall` | 证据 | | --- | --- | --- | --- | | 注册表 | 15 项(含 `cloud`) | 14 项(无 `cloud`) | `pkgs/all/internal/service/service.go:14-33` vs `pkgs/ecmall/internal/service/service.go:14-32` | | 端口 | 12000 / 12001 | 12300 / 12301 | 两侧 `etc/default_dev.yaml:3-8` | | `Fts.Local.Site` | `http://127.0.0.1:12001/files` | `http://127.0.0.1:12301/files` | `pkgs/all/etc/default_dev.yaml:73` vs `pkgs/ecmall/etc/default_dev.yaml:79` | | `internal/server/*` | —— | **完全相同**(仅 `server.go:79-80` 的日志字面量 `all`/`ecmall` 不同) | `diff` 结果 | | `internal/config/config.go` | —— | 仅多一段顶部注释(`config.go:20-25`) | `diff` 结果 | | `internal/impl/impl.go` | —— | 仅 import 路径不同 | `diff` 结果 | | 注册表测试 | 无 | `internal/service/service_test.go`(62 行) | 实测 `go test ./pkgs/all/...` 输出 `internal/service [no test files]` | | 配置一致性断言 | 只校验非空 | 额外断言 `Services` == `ecmallServices` 常量 | `internal/config/config_test.go:15,43-52` | | `Cloud` 配置节 | 无(cloud 无专项配置) | 无 | 两侧 `SrvConfig` 均无 `Cloud` 字段 | `internal/config/config.go:20-25` 的注释明确了这个取舍:「Modules that are not part of ecmall keep their own configuration in their own process」。 ## 2. 代码结构与关键文件 | 路径 | 行数 | 职责 | | --- | --- | --- | | `cmd/main/main.go` | 58 | 进程入口:`--workspace` → `config.New` → `impl.NewImpl` → `server.New` → `service.Expose` → 信号 → `srv.Start` | | `internal/config/config.go` | 130 | 配置结构 + 监听地址规范化 + 密钥校验 + 共享配置回填 + `Enabled` | | `internal/config/config_test.go` | 53 | dev 配置非空、端口不同、密钥长度、5 个专项节存在,且 `Services` 必须逐项等于 `ecmallServices` | | `internal/server/server.go` | 133 | gRPC server(unary 拦截器 + reflection)、gateway mux、gin engine、动态路由、404 兜底、`*http.Server` 参数、`Stop` | | `internal/server/authorization.go` | 126 | HS256 JWT 校验、HTTP 中间件、gRPC unary 拦截器、匿名白名单、`/rpc/...` 路径规范化 | | `internal/server/dynamic.go` | 194 | 动态 RPC(reflection 取 descriptor + `dynamicpb` + `protojson` + 内网 `Invoke`) | | `internal/server/response.go` | 78 | 统一错误体与 gRPC code → errcode 映射 | | `internal/impl/impl.go` | 42 | 进程级共享资源(Memory/Redis/DB/Etcd) | | `internal/service/service.go` | 45 | 注册表(14 项)+ session 中间件 + 逐个 `Expose` | | `internal/service/service_test.go` | 62 | 注册表护栏:断言 11 个模块前缀的 gRPC 服务 + 6 条 REST 路由确实注册成功 | | `internal/service/{ads,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` | 151 | 唯一一份配置 | | `AGENT.md` | 776 | ecmall 开发上下文(§3.5 记录聚合不建表不播种、§4.5 记录匿名口径差异、§5 服务注册表) | | `go.mod` | 146 | 14 个 module replace 到 `../../module/...`,`bsm-sdk/core` → `../../../../bsm-sdk/core` | 注册表(`internal/service/service.go:14-32`):`ads, cms, feedback, fts, initial, logs, mgt, passport, sender, address, mall, market, order, wallet`,与 `etc/default_dev.yaml:50-64` 的 `Services` 逐项一致(已核对,并被 `config_test.go:43-52` 强制)。 ## 3. 启动流程与请求分流 启动顺序(`cmd/main/main.go:22-57`):`--workspace` 校验 → `config.New`(读 `ecmall/etc/default_.yaml`、归一化地址、校验两地址不同与密钥、写入 `env.JwtSecretKey`/`coreVars.JwtExpire`、回填共享配置)→ `impl.NewImpl()`(Memory/Redis/DB/Etcd)→ `ecmallserver.New`(gRPC server + reflection + gin engine)→ `service.Expose`(session 中间件 + 注册表逐个注册,失败即 panic)→ `srv.Start` → `SIGINT/SIGTERM` → `srv.Stop`。 分流实现与 `pkgs/all` 完全一致(两个文件逐字节相同),因此下文只标注 ecmall 侧行号: ```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(路由未命中 或 业务 NotFound)"| 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"] H -->|"无路由"| N["gin 默认 404(纯文本)"] D2 --> E2["内网 gRPC 再经 unary 拦截器校验 JWT
authorization.go:42-53"] ``` | 入口 | 形态 | ecmall 覆盖范围 | 鉴权节点 | | --- | --- | --- | --- | | 原生 gRPC | `/{pkg}.{Svc}/{M}` | 11 个 gRPC 模块(fts/logs/mgt 无 gRPC 面) | gRPC unary 拦截器(`server.go:32`) | | grpc-gateway | `POST /{pkg}.{Svc}/{M}` | 同 11 个模块 | HTTP 中间件 + 内网 unary 拦截器 | | 动态 RPC | `POST /rpc/{pkg}/{Svc}/{M}` | 同 11 个模块,仅 unary | HTTP 中间件(按网关形态匹配白名单)+ 内网 unary 拦截器 | | 原生 REST | `/rest/{fts,logs,mgt}/...` | 仅这 3 个模块 | HTTP 中间件 + 模块内层 `JwtAuth`(`/rest/fts/uploader`、`/rest/mgt/{user,app,role,pmn,dpt}/*`) | ## 4. 配置与安全 | 项 | ecmall 现状 | 证据 | | --- | --- | --- | | 监听地址 | gRPC `12300` / HTTP `12301`,`BindIP 0.0.0.0`;两地址相同则 panic | `etc/default_dev.yaml:3-8`;`config.go:66-68` | | 端口缺省 | `Port` 为空时随机 1024-65535(SDK 行为),两地址必然不同 | SDK `conf/new.go:87-97`;`config.go:91-95` | | JWT | HS256,裸 JWT 放 `Authorization` 头(无 `Bearer`),校验 `exp`/`iat`/`now-iat ≤ Expire` | `authorization.go:68-94`;`authorization_test.go:36-41` | | JWT 密钥 | `Authorization.Key` 长度 ∈ {16,24,32},写入全局 `env.JwtSecretKey` 供模块签发共用 | `config.go:72-80` | | 默认密钥 | `CHANGE_ME_32_BYTE_JWT_SECRET_KEY`(32 字节,直接通过校验) | `etc/default_dev.yaml:22`;`config.go:72-75` | | session 密钥 | `config.Spec.SecretKey`(`conf.Base`)拼 `"-session"`,配置值 `CHANGE_ME`,**无校验** | `internal/service/service.go:35`;`etc/default_dev.yaml:17`;`config.go:69-78` 未校验 | | 匿名白名单 | 19 条(11 条网关形态 + 8 条 REST 路径),`/grpc.reflection.`、`/grpc.health.` 前缀硬编码放行 | `etc/default_dev.yaml:24-43`;`authorization.go:96-103` | | gRPC reflection | 无条件注册、无开关、匿名可达 | `server.go:33`;`authorization.go:98-100` | | 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 默认信任全网段) | `server.go:34`;gin `gin.go:225,214` | | 限流 | 无 | `grep -i "ratelimit\|limiter\|throttle" pkgs` 无命中 | | 运行模式 | gin 未按 `BSM_RuntimeMode` 设置 | `server.go:34` | | 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` | | 建表/播种 | 聚合不执行:`applyDependencies` 只覆盖包变量(`module/base/mgt/service/dependencies.go:19-32`),不含 `models.New`/`AutoMigrate`/`InitRootUserData`(唯一调用链是 `module/base/mgt/internal/impl/with.go:20` ← 模块自己的 `impl.NewImpl()`) | `grep -rn "NewImpl()\|AutoMigrate\|models.New(" module/*/*/service/*.go` 无命中 | 匿名白名单逐条核对:11 条 gRPC 条目全部对应真实方法(`module/base/passport/proto/{login,register,forget,verify}.proto`、`module/ec/mall/proto/staff.proto:9`、`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`)——**无死条目,问题在"该放行的没放行"**(见 5.1)。 `AGENT.md` 与代码的一致性:`AGENT.md:193-216` 的《匿名白名单现状》表与 `etc/default_dev.yaml:24-43` 逐条相符,并主动列出"模块声明匿名但聚合不放行"的 4 组接口(ads.Fetch/ByPos、sender 三个方法、feedback 四个方法、logs 三个 REST)。**文档是诚实的,但没有任何代码或测试强制两者一致**(见 5.3)。 ## 5. 审计发现 ### 5.1 安全 | 级别 | 位置 | 问题 | | --- | --- | --- | | **高** | `etc/default_dev.yaml:22` + `internal/config/config.go:72-75` | **JWT 签名密钥默认值是可直接使用的公开字面量**。`CHANGE_ME_32_BYTE_JWT_SECRET_KEY` 恰好 32 字节 → 通过长度校验 → 写入 `env.NewEnv().JwtSecretKey`(`config.go:79`)。`authorization.go:74-79` 只校验 HS256 签名与 `iat/exp`,因此知道该字面量即可自行签发任意身份的 token;模块侧签发也用同一密钥(`module/base/mgt/internal/logic/pub/login.go:90`、`module/base/passport/internal/logic/common/token.go:10`),无法单侧规避。 | | **高** | `etc/default_dev.yaml:17` + `internal/service/service.go:35` | **session 密钥来自未校验的 `SecretKey`**。`cookie.NewStore([]byte(config.Spec.SecretKey+"-session"))`,配置值 `CHANGE_ME`;`config.New` 只校验 `Authorization.Key`(`config.go:69-78`),对 `SecretKey` 从不校验。默认配置下 `mysession` cookie 的 HMAC 密钥可猜(当前唯一使用点是 mgt 的 SessionDemo 演示路由)。 | | **高** | `pkgs/ecmall/etc/`(仅 1 个文件) | **不存在生产配置**。`BSM_RuntimeMode=prod` 时读 `default_prod.yaml`,缺失即回退 `workspace_default_prod.yaml`(SDK `conf/new.go:35-43`),两者都不存在 → `log.Fatalf`(`new.go:49-52`)。即聚合入口只能以 dev 模式启动;`scripts/build-all-linux.sh` 又只扫描 `module/`(`:7,30`),聚合既无生产配置也不在构建范围内。 | | **中** | `internal/server/server.go:33` + `authorization.go:96-100` | **reflection 无开关且匿名可达**:匿名可枚举 11 个模块的全部 gRPC 服务/方法与完整消息结构(动态 RPC 自身就依赖它,`dynamic.go:124-143`)。 | | **中** | `internal/server/authorization.go:98` | `/grpc.health.` 硬编码匿名,但没有任何模块注册 health 服务(`grep -rn "health" module` 只命中注释与字符串)→ 当前是死名单,将来补 health 服务会先天匿名。 | | **中** | `internal/server/server.go:45,49` + `dynamic.go:45` | **全链路明文**,无 TLS 配置能力;JWT 与业务数据在 `0.0.0.0:12300/12301` 明文传输(是否由前置代理终止 TLS 属部署前提,进程内无保障)。 | | **中** | `internal/server/server.go:34`、`dynamic.go:180-188` | **客户端 IP 可伪造**:未 `SetTrustedProxies`,gin v1.12.0 默认 `trustedProxies={"0.0.0.0/0","::/0"}` 且 `ForwardedByClientIP=true`(gin `gin.go:225,214`)→ `c.ClientIP()` 取客户端自报 XFF;动态 RPC 把请求中所有 `x-` 前缀头原样透传内网(`dynamic.go:182-188`);grpc-gateway 也会把请求 XFF 拼入 metadata(gateway `runtime/context.go:185-193`)。落点示例:`module/base/mgt/internal/logic/pub/login.go:90` 把 client IP 写进 token。 | | **中** | `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 校验。 | | **中** | `etc/default_dev.yaml:24-43` 对比模块实现 | **匿名口径与模块声明不一致(ecmall 侧比 all 多一项)**:① `logs` 的 `POST /rest/logs/create|fetch|total` 在模块里注册于"匿名组"(`module/base/logs/internal/routers/register.go:18-29`,模块自身 `JwtAuth` 被注释掉)→ 聚合下要求 JWT。② `ads.Fetch/ByPos`(`module/base/ads/etc/ads_dev.yaml:15-16` 声明匿名)、`sender.Sms/Send`、`sender.Sms/Verify`、`sender.Mail/Send`、`feedback.Method/{List,Get,Delete,Remark}` 在模块侧匿名或不做归属校验,聚合白名单均不含 → 上线 401。③ `cloud` 不在 ecmall 注册表内,即使 `BSM_SERVICES=cloud` 也会被静默忽略(`config.Enabled("cloud")` 返回 true,但注册表里没有对应 `expose` 项)。 | | **低** | `internal/server/authorization.go:96-120` | 白名单是精确字符串匹配,gRPC 形态与 REST 真实路径混列同一列表;`/rpc/...` 仅在恰好 4 段时改写为网关形态(`canonicalHTTPPath`)。新增方法/路由忘记登记只会静默变成"需要 JWT",无一致性校验或告警。 | | **低** | `internal/server/dynamic.go:73,115-176` | 动态 RPC 描述符缓存键由请求路径拼接(无上限、无 TTL),未命中时每次向内网反射服务发一次 RPC;任意匿名调用者可用随机符号名放大内网调用,`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`("记录不存在"等高频分支,如 `mall.Product`/`order.Summary`/`cms.Post` 的查询)时,已生成的 `{code,message,...}` 响应被丢弃,客户端收到 gin 的纯文本 `404 page not found`。 | | **中** | `internal/server/server.go:82-89` | **启动期错误只处理一半**:`errCh` 只读一个值即返回,先失败的一方不会关闭另一侧的 listener/gRPC server,也没有 `Shutdown` 兜底。 | | **中** | `cmd/main/main.go:48-53` + `internal/server/server.go:92-110` | **优雅关闭超时被复用、错误被丢弃**:`GracefulStop` 超时后用同一个已过期的 ctx 执行 `s.http.Shutdown(ctx)`,立即返回 deadline exceeded,在途 REST 请求被中断;外层 `_ = srv.Stop(ctx)` 丢弃返回错误。 | | **中** | `internal/server/server.go:32` | **流式 RPC 不受鉴权覆盖**:只有 `grpc.UnaryInterceptor`,无 `StreamInterceptor`。当前 66 个 proto、35 个服务无 `stream` 声明(`grep -n "stream " --include=*.proto module` 无命中),故无实际暴露;动态 RPC 侧已显式拒绝流式(`dynamic.go:79-82`),风险在原生 gRPC 面(`12300`)。 | | **中** | `internal/server/server.go:112-133` | **全量缓冲响应**:所有 gateway 响应先写入 `bytes.Buffer` 再整体 flush(`server.go:64,125-132`);`cms.Post.List`、`mall.Product`、`order.Summary.List` 这类列表响应在内存中同时存在两份;`flush` 用 `Add` 复制响应头(`server.go:128`);`WriteHeader` 无重复调用保护(`server.go:123`)。 | | **中** | `etc/default_dev.yaml:79` + `module/base/fts/internal/logic/provider.go:71` | **配置的下载地址没有对应路由**:`Fts.Local.Site: http://127.0.0.1:12301/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 在 `12301` 上必然 404。 | | **中** | `internal/config/config.go:92-95` | **端口漏配会随机绑定**:`conf.CheckPort` 在 `Port` 为空时随机生成 1024-65535(SDK `conf/new.go:90-97`),两个 listener 各自随机且"必须不同"的校验必然通过 → 在随机端口"成功"启动;`ecmall` 的随机端口还可能撞上 `pkgs/all` 的 `12000/12001` 或模块 dev 端口。 | | **低** | `internal/config/config_test.go:15,43-52` | **注册表清单存在 3 处拷贝**:`internal/service/service.go:14-32`(注册表本体)、`internal/config/config_test.go:15`(`ecmallServices` 常量)、`etc/default_dev.yaml:50-64`(`Services`)。测试只能保证后两者一致;**新增服务**若漏改常量与 yaml,两项测试都不会失败(`service_test.go:31` 只断言 11 个固定前缀存在,不断言"注册表 == yaml")。`AGENT.md:247` 已把"改 3 处"写成约定,属人工约束。 | | **低** | `internal/config/config.go:115-130` + `internal/service/service.go:34-45` | **服务名写错会静默启动空宿主**:`Enabled` 不校验名字是否存在,`Expose` 对 0 个注册不报错;`BSM_SERVICES=mail`(mall 的笔误)会得到一个只在 `12300/12301` 上回答 404 + reflection 的进程。 | | **低** | `cmd/main/main.go:23-29` | workspace 名会被 `utils.MustString` 转成小写(SDK `utils/ext.go:40-41`),因此 `--workspace=Tenant01` 会去找 `tenant01_dev.yaml`;README 描述一致,但配置文件命名必须全小写,否则报错信息(`workspace must match ...`)与实际文件名不匹配,易误判。 | ### 5.3 未完成/不一致 | 级别 | 位置 | 问题 | | --- | --- | --- | | **高** | `pkgs/ecmall/etc/` + `scripts/build-all-linux.sh:7,30` | **无 prod/test 配置,且不在构建脚本范围内**:`MODULE_ROOT="${WORKSPACE_ROOT}/module"`,`pkgs/` 完全不被扫描,`pkgs/ecmall/etc` 只有 `default_dev.yaml`。上线路径目前只有"开发模式 + 手工 `go build`"。 | | **中** | `etc/default_dev.yaml:86-88` + `module/base/mgt/internal/models/impl.go:61-74` | **聚合下 `Mgt.InitRootUser: true` 不生效**。建表与 root 播种唯一调用链是 `module/base/mgt/internal/impl/with.go:20` ← 模块自己的 `impl.NewImpl()`;聚合只调 `applyDependencies`(`module/base/mgt/service/dependencies.go:19-32`),`grep -rn "NewImpl()\|AutoMigrate\|models.New(" module/*/*/service/*.go` 无命中。yaml 已用注释说明(`default_dev.yaml:68-70` 指向 `AGENT.md §3.5`),但配置项本身仍在,容易被误读为"已开启初始化"。 | | **中** | `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」,不校验签名。当前无实际危害,补实现前必须先加签名校验。 | | **中** | `internal/config/config.go:84` + 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:31-44`),此时 Redis/Etcd 已建连且无失败回滚。 | | **低** | `internal/server/*` vs `pkgs/all/internal/server/*` | **两份拷贝需手工同步**:`server.go`/`authorization.go`/`dynamic.go`/`response.go` 逐字节相同(唯一差异是 `server.go:79-80` 的日志字面量),没有测试或 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`),ecmall 只有三个模块自带的 `/rest/*/ping`,`HEAD /` 落到 gin 404。 | | **低** | `AGENT.md:110` | 文档引用行号漂移:`AGENT.md` 说 `SecretKey` 被 `internal/service/service.go:29` 使用,实际在 `:35`。文档正文正确、行号过期,属长期维护噪音(同类引用见 §4.2/§4.4 等处的行号)。 | ### 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 可用,任何流式/大文件网关响应都会静默退化。 | | **低** | `internal/server/dynamic.go:34-36` | 描述符缓存无条件增长(`map` + `RWMutex`,无上限、无 TTL),键来自外部输入的路径拼接。 | | **低** | `internal/config/config.go:115-130` | `BSM_SERVICES` 一旦非空即**完全覆盖** `Services`,且无去重、无存在性校验;`Enabled` 同时对 `"all"` 特判,语义("全开"与"名单"混用)只能从 README/AGENT 得知。 | | **低** | 测试覆盖 | 有护栏但不深:`service_test.go:15-53` 断言 11 个前缀与 6 条 REST 路由存在,`config_test.go` 断言 yaml 与常量一致;但**没有**对 `httpMiddleware` 白名单与注册表一致性的断言、没有对 404 分流语义的断言(本次 E4 的 404 覆盖问题因此未被测试发现)。 | ## 6. 风险汇总 | 编号 | 级别 | 问题 | 影响面 | | --- | --- | --- | --- | | E1 | 高 | JWT 密钥默认值是可直接使用的公开字面量 | 全系统身份伪造 | | E2 | 高 | session 密钥取未校验的 `SecretKey`(默认 `CHANGE_ME`) | cookie 完整性与伪造 | | E3 | 高 | 无 prod 配置,且构建脚本不覆盖 `pkgs/*` | 上线路径、部署可靠性 | | E4 | 高 | gateway 404 与业务 `NotFound` 不可区分,真实响应被覆盖 | 所有 gateway 方法的错误语义 | | E5 | 中 | reflection 无开关且匿名可达(该入口依赖它) | 内部 API 全貌泄露 | | E6 | 中 | 无 TLS | 传输机密性 | | E7 | 中 | 客户端 IP 可经 XFF 伪造并透传内网 metadata | 审计与风控数据可信性 | | E8 | 中 | 匿名登录入口无任何限流 | 账号安全 | | E9 | 中 | 匿名口径与模块声明不一致(logs 三条 REST、ads/sender/feedback 多组接口) | 独立/聚合行为不一致、上线 401 | | E10 | 中 | 流式 RPC 不受鉴权拦截器覆盖(当前无流式方法) | 未来扩接口时的越权风险 | | E11 | 中 | `Fts.Local.Site` 指向的 `/files` 无路由 | 文件下载不可用 | | E12 | 中 | 优雅关闭超时后 HTTP 立即中断且错误被丢弃 | 关闭期请求可靠性 | | E13 | 中 | gateway 响应全量缓冲 | 大响应下的内存放大 | | E14 | 中 | 聚合不建表不播种,`Mgt.InitRootUser: true` 不生效 | 首次部署缺表/缺管理员 | | E15 | 中 | `Databases` 未校验 + 资源初始化无失败回滚 | 启动期崩溃与连接泄漏 | | E16 | 中 | Jumio 回调空壳(无签名校验)却已匿名放开 | 补实现后的数据伪造风险 | | E17 | 低 | 注册表 3 处拷贝、服务名写错静默空宿主、两份 server 拷贝需手工同步 | 变更回归与一致性 | | E18 | 低 | 端口漏配随机绑定、无 WriteTimeout、gin 模式/CORS 与独立入口不一致、无健康端点、文档行号漂移 | 运维与可维护性 | ## 7. 修复建议(务实项) 1. **密钥不能是占位值**(E1、E2):在 `internal/config/config.go` 现有 `Authorization.Key` 校验之后,加"`Authorization.Key` 不得等于样例字面量"与"`SecretKey` 非空且不得等于 `CHANGE_ME`"两条判断(各 3 行);顺带把 `Databases` 纳入 `conf.NotNil`(E15)。不引入配置中心或加密方案。 2. **补生产配置与构建覆盖**(E3):新增 `pkgs/ecmall/etc/default_prod.yaml`(替换全部 `CHANGE_ME`、显式 `BindIP`),并让 `scripts/build-all-linux.sh` 的模块收集范围覆盖 `pkgs/`(E3 与工作区脚本报告同源)。 3. **修 404 分流**(E4):自定义 gateway 的 routing error handler(或让 `bufferedResponse` 记录"是否由 routing error handler 写出"),只在"路由未命中"时回退 gin;业务 `NotFound` 必须原样 flush。这是 `server.go:63-71` 一处判断的修正,不需要改接口契约。 4. **reflection 开关化**(E5):给 `Server.New` 增加一个布尔形参/配置项(默认关闭),仅在开启时 `reflection.Register`;同时从 `isAnonymous` 删除 `/grpc.health.` 前缀。注意动态 RPC 依赖反射——若要保留 `/rpc/...`,应改为只允许来自 loopback 的调用(`remoteAddr` 判定即可)。 5. **客户端 IP 与头部清理**(E7):`server.New` 内 `engine.SetTrustedProxies(<真实代理网段>)`(无代理传 `nil`);`outgoingMetadata` 不再透传客户端自带的 `x-forwarded-for`/`x-real-ip`,改为服务端重算。 6. **匿名白名单口径对齐**(E9):逐条决策后同步两处——logs 的 `create/fetch/total`、ads 的 `Fetch/ByPos`、sender 的三个方法、feedback 的四个方法,要么加入 `Authorization.Anonymous`,要么在模块侧去掉"匿名"声明。改配置即可,零代码。 7. **文件下载路由**(E11):在 fts 路由注册补静态目录(本地存储场景),或把 `Fts.Local.Site` 改成真实存在的前缀。 8. **启动/关闭收尾**(E12、E15):`Start` 中任一侧 `Serve` 返回错误时关闭两个 listener 并 `Shutdown`;`Stop` 中给 `http.Shutdown` 单独的短超时 ctx 并输出错误;`Port` 为空改为报错退出,不要随机绑定(E18)。 9. **注册表与测试护栏**(E17):在 `config_test.go` 增加"yaml 的 `Services` 与注册表逐项一致"的断言(现只能比对常量);在 `service_test.go` 补一条"白名单中的 REST 路径确实已注册"的断言,把 `AGENT.md §4.5` 的记录变成可执行检查。 10. **两份拷贝的同步**(E17):在 `pkgs/all` 补与 `pkgs/ecmall/internal/service/service_test.go` 等价的注册表测试;两份 `internal/server` 的漂移用"同一组测试断言 + 文件头注明需同步"来兜,不新建公共包。 11. **首次部署前置**(E14):在 `README.md`/`AGENT.md` 之外,把"聚合不建表、不播种 root"写成启动期显式检查(例如启动时探测 mgt 关键表是否存在,缺失则拒绝启动并打印指引),避免运行到首个请求才暴露。 12. **入口限流与可观测**(E8、E18):在 `httpMiddleware` 内对匿名路径加一个基于内存的简单计数限流;`http.Server` 补 `WriteTimeout`;按 `BSM_RuntimeMode` 设置 gin 模式;注册 `HEAD /` 健康端点。 > 本报告只列与现有实现直接相关的修复项,不引入新的分层或抽象封装。审计中"合并两个聚合入口""为聚合引入配置中心/服务注册中心""把动态 RPC 改造成网关插件"一类改造不在此列。