30 KiB
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_<mode>.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 侧行号:
flowchart TD
C["客户端请求"] --> A["httpMiddleware:canonicalHTTPPath + 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(路由未命中 或 业务 NotFound)"| H["gin ServeHTTP 兜底<br/>server.go:70"]
H --> R1["POST /rpc/:module/:service/:method<br/>server.go:61"]
R1 --> D["resolveMethod:gRPC reflection 取 descriptor(带缓存)<br/>dynamic.go:115-176"]
D --> D1["streaming → Unimplemented<br/>dynamic.go:79-82"]
D --> D2["protojson 解 JSON(≤4 MiB)→ 内网 gRPC Invoke(insecure)<br/>dynamic.go:84-112"]
H --> R2["/rest/fts/... /rest/logs/... /rest/mgt/...(模块自注册)<br/>service/{fts,logs,mgt}.go"]
H -->|"无路由"| N["gin 默认 404(纯文本)"]
D2 --> E2["内网 gRPC 再经 unary 拦截器校验 JWT<br/>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 |
| 中 | etc/default_dev.yaml:24-43 对比模块实现 |
匿名口径与模块声明不一致(ecmall 侧比 all 多一项):① logs 的 `POST /rest/logs/create |
| 低 | 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 |
| 中 | 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. 修复建议(务实项)
- 密钥不能是占位值(E1、E2):在
internal/config/config.go现有Authorization.Key校验之后,加"Authorization.Key不得等于样例字面量"与"SecretKey非空且不得等于CHANGE_ME"两条判断(各 3 行);顺带把Databases纳入conf.NotNil(E15)。不引入配置中心或加密方案。 - 补生产配置与构建覆盖(E3):新增
pkgs/ecmall/etc/default_prod.yaml(替换全部CHANGE_ME、显式BindIP),并让scripts/build-all-linux.sh的模块收集范围覆盖pkgs/(E3 与工作区脚本报告同源)。 - 修 404 分流(E4):自定义 gateway 的 routing error handler(或让
bufferedResponse记录"是否由 routing error handler 写出"),只在"路由未命中"时回退 gin;业务NotFound必须原样 flush。这是server.go:63-71一处判断的修正,不需要改接口契约。 - reflection 开关化(E5):给
Server.New增加一个布尔形参/配置项(默认关闭),仅在开启时reflection.Register;同时从isAnonymous删除/grpc.health.前缀。注意动态 RPC 依赖反射——若要保留/rpc/...,应改为只允许来自 loopback 的调用(remoteAddr判定即可)。 - 客户端 IP 与头部清理(E7):
server.New内engine.SetTrustedProxies(<真实代理网段>)(无代理传nil);outgoingMetadata不再透传客户端自带的x-forwarded-for/x-real-ip,改为服务端重算。 - 匿名白名单口径对齐(E9):逐条决策后同步两处——logs 的
create/fetch/total、ads 的Fetch/ByPos、sender 的三个方法、feedback 的四个方法,要么加入Authorization.Anonymous,要么在模块侧去掉"匿名"声明。改配置即可,零代码。 - 文件下载路由(E11):在 fts 路由注册补静态目录(本地存储场景),或把
Fts.Local.Site改成真实存在的前缀。 - 启动/关闭收尾(E12、E15):
Start中任一侧Serve返回错误时关闭两个 listener 并Shutdown;Stop中给http.Shutdown单独的短超时 ctx 并输出错误;Port为空改为报错退出,不要随机绑定(E18)。 - 注册表与测试护栏(E17):在
config_test.go增加"yaml 的Services与注册表逐项一致"的断言(现只能比对常量);在service_test.go补一条"白名单中的 REST 路径确实已注册"的断言,把AGENT.md §4.5的记录变成可执行检查。 - 两份拷贝的同步(E17):在
pkgs/all补与pkgs/ecmall/internal/service/service_test.go等价的注册表测试;两份internal/server的漂移用"同一组测试断言 + 文件头注明需同步"来兜,不新建公共包。 - 首次部署前置(E14):在
README.md/AGENT.md之外,把"聚合不建表、不播种 root"写成启动期显式检查(例如启动时探测 mgt 关键表是否存在,缺失则拒绝启动并打印指引),避免运行到首个请求才暴露。 - 入口限流与可观测(E8、E18):在
httpMiddleware内对匿名路径加一个基于内存的简单计数限流;http.Server补WriteTimeout;按BSM_RuntimeMode设置 gin 模式;注册HEAD /健康端点。
本报告只列与现有实现直接相关的修复项,不引入新的分层或抽象封装。审计中"合并两个聚合入口""为聚合引入配置中心/服务注册中心""把动态 RPC 改造成网关插件"一类改造不在此列。