Files
full/docs/pkgs-all.md
2026-09-22 18:53:53 +08:00

32 KiB
Raw Blame History

pkgs/all全量聚合宿主代码审计报告

内容
审计对象 pkgs/allmodule 路径 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:12001pkgs/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.applyDependenciesmodule/ec/order/service/dependencies.go:19-29)。

pkgs/ecmall 的关系:两者是同一份代码的两份拷贝internal/server 下 4 个文件逐字节相同(见 6.3-C5注册表仅差 cloud 一项。

2. 代码结构与关键文件

路径 行数 职责
cmd/main/main.go 58 进程入口:--workspace 解析 → config.Newimpl.NewImplserver.Newservice.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/GatewayEngine+(可选 Config
etc/default_dev.yaml 145 唯一一份配置端口、DB/Redis DSN、SecretKeyAuthorization(含匿名白名单)、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-3315 项,顺序即注册顺序):ads, cloud, cms, feedback, fts, initial, logs, mgt, passport, sender, address, mall, market, order, wallet。与 etc/default_dev.yaml:46-61Services 列表逐项一致(已核对)。

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.JwtSecretKeycoreVars.JwtExpire → 回填共享配置(pkgs/all/internal/config/config.go:56-83)。
  3. impl.NewImpl()Memory → Redis → DB → Etcdinternal/impl/impl.go:30-42)。注意 DB 缺失时 SDK 直接 panic(见 6.3-C3
  4. allserver.New(...):建 gRPC server无条件 reflection.Register+ gin enginegin.New() + Logger + Recovery
  5. service.Expose(srv):先挂 session 中间件,再按注册表 + Enabled 逐个注册;任一模块 Expose 出错则整体 panicinternal/service/service.go:35-46cmd/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

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 12001BindIP 0.0.0.0;两地址相同则 panic etc/default_dev.yaml:3-8config.go:60-62
端口缺省 Port 为空时随机分配 1024-65535,且"两地址不同"校验必然通过 SDK conf/new.go:87-97config.go:85-89
JWT HS256裸 JWT 放 Authorization 头(无 Bearer);校验算法、expiat 不为未来、now-iat ≤ Expire authorization.go:68-94authorization_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:22config.go:66-69
session 密钥 config.Spec.SecretKey= conf.Base.SecretKey)拼 "-session" 作为 cookie store 的 HMAC key无任何校验 internal/service/service.go:36SDK conf/types.go:11config.go:63-72 未校验
默认 session 密钥 CHANGE_ME etc/default_dev.yaml:17
鉴权中间件覆盖 HTTP单一中间件包住"gateway + gin"整个 handler因此 /rpc/.../rest/... 全部在覆盖内gRPCgrpc.UnaryInterceptor(无 Stream server.go:32,74authorization.go:55-66
匿名白名单 11 个 gRPC 网关形态 + 8 个 REST 路径,共 19 条;/grpc.reflection./grpc.health. 前缀硬编码放行 etc/default_dev.yaml:24-43authorization.go:96-103
gRPC reflection 无条件注册、无开关;且被 isAnonymous 放行为匿名 server.go:33authorization.go:98-100
TLS 全模块无 TLSnet.Listen("tcp", ...)、内网 insecure.NewCredentials()grep -i "tls|certificate|x509" pkgs 无命中 server.go:45,49dynamic.go:45
HTTP server 参数 ReadHeaderTimeout=10sIdleTimeout=120sMaxHeaderBytes=1MiBReadTimeout/WriteTimeout server.go:72-78
gRPC server 参数 无 MaxRecvMsgSize、无 keepalive、无 StreamInterceptor server.go:32
客户端 IP gin.New()SetTrustedProxiesgin v1.12.0 默认信任 0.0.0.0/0::/0 server.go:34gin 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:34SDK middleware/mode.go:9-16
CORS 聚合未启用;独立 fts/mgt 启用SDK 实现为 AllowAllOrigins: true server.go:34module/base/fts/cmd/main/main.go:26module/base/mgt/cmd/main/main.go:39SDK middleware/cors.go:8-18

匿名白名单逐条核对结果11 条 gRPC 条目全部对应真实方法(passportLogin.{Pwd,Code,Quick}Register.{Pwd,Code}Forget.{Verify,Reset}Verify.{Request,JumioCallback}module/base/passport/proto/*.protomall.Staff/Loginmodule/ec/mall/proto/staff.proto:9market.Agency/Loginmodule/ec/market/proto/agency.proto:98 条 REST 条目也全部对应已注册路由(module/base/fts/internal/routers/register.go:22-23module/base/logs/internal/routers/register.go:24module/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.BaseSDK conf/types.go:11),配置值为 CHANGE_MEconfig.New 只校验 Authorization.Keyconfig.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().JwtSecretKeyconfig.go:73)。任何知道该字面量的人都能自行 HS256 签发带 iat/exp 的 token通过 authorization.go:74-79 的校验。模块的 token 也由同一密钥签发(module/base/mgt/internal/logic/pub/login.go:90module/base/passport/internal/logic/common/token.go:10),无法通过"只改某一侧"缓解。
pkgs/all/etc(目录仅 1 个文件) 不存在生产配置conf.New<Prefix>/etc/default_<mode>.yamlSDK conf/new.go:35-36BSM_RuntimeMode 默认 devSDK env/env.go:20);设为 prod 时读 default_prod.yaml,缺失即回退 workspace_default_prod.yamlnew.go:39-43),两者都不存在 → log.Fatalfnew.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()pkgsgrep -i "tls|certificate|x509" 无任何命中。JWT 与业务数据以明文在 0.0.0.0:12000/12001 上传输,反向代理是否终止 TLS 属于部署前提,进程内无任何能力。
internal/server/server.go:34dynamic.go:180-188 进程级客户端 IP 可被伪造。聚合用 gin.New() 且未调用 SetTrustedProxiesgin v1.12.0 默认 trustedProxies = {"0.0.0.0/0","::/0"}ForwardedByClientIP=truegin gin.go:225,214),因此 c.ClientIP() 直接采用客户端自报的 X-Forwarded-For;动态 RPC 又把请求里所有 x- 前缀头原样透传给内网 gRPCdynamic.go:182-188grpc-gateway 也会把请求 X-Forwarded-For 拼进 metadatagateway runtime/context.go:185-193)。受影响落点示例:登录 token 里记录的 client IPmodule/base/mgt/internal/logic/pub/login.go:90)。
internal/server/authorization.go:55-66server.go:35 匿名登录入口无任何限流。白名单里的 `passport.Login/Pwd
etc/default_dev.yaml:24-43 对比 module/base/logs/internal/routers/register.go:18-29module/base/ads/etc/ads_dev.yaml:15-16 匿名口径不一致(双向)。① logs 的 `POST /rest/logs/create
internal/server/authorization.go:96-120 白名单为精确字符串匹配,且 gRPC 网关形态与 REST 真实路径混列在同一 []string 里;/rpc/... 只在"恰好 4 段"时才被改写成网关形态(canonicalHTTPPath)。新增方法/路由若忘记登记,只会静默变成"需要 JWT",没有任何一致性校验或启动期告警。
internal/server/dynamic.go:115-176 动态 RPC 的 descriptor 缓存键由请求路径拼接moduleName + "." + serviceShortNamedynamic.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),与路由未命中的 404runtime/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(反之亦然);StartShutdown 兜底。
cmd/main/main.go:48-53 + internal/server/server.go:92-110 优雅关闭的超时被复用且错误被丢弃Stop(ctx) 内部先用同一个 ctxGracefulStop,超时后 s.GRPC.Stop(),随后 s.http.Shutdown(ctx) 使用的仍是已经过期的 ctx → 立即返回 context deadline exceeded,在途 REST 请求被直接中断;外层 _ = srv.Stop(ctx) 又丢弃了返回值,失败完全无声。
internal/server/server.go:112-133 全量缓冲响应。所有 grpc-gateway 响应先写入 bytes.Bufferserver.go:64),判定不是 404 后才整体 flushserver.go:125-132响应体在内存中同时存在缓冲与写出两份列表类接口mall 商品、order 订单、cms 文章)在大响应/高并发下放大内存占用;flush 复制响应头用 Add 而非 Setserver.go:128WriteHeader 无重复调用保护(server.go:123)。
internal/server/server.go:32 流式 RPC 无鉴权拦截。鉴权只挂在 grpc.UnaryInterceptor,未注册 StreamInterceptorisAnonymous 虽放行 /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
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-33etc/default_dev.yaml:46-61
internal/config/config.go:109-124 + internal/service/service.go:35-46 服务列表写错会静默启动空宿主Enabled 只做"名字匹配/含 all"判断,不校验名字是否存在;Expose 对 0 个服务注册不报错。若 BSM_SERVICESServices 里全是无效名(例如把 mall 写成 mail),进程会正常监听 12000/12001,但除 reflection 外全部 404且没有任何启动期提示。
internal/config/config.go:85-89 端口漏配会随机绑定conf.CheckPortPort 为空时随机生成 1024-65535SDK conf/new.go:90-97);聚合有两个 listenerPort 缺失时各自随机,"两地址必须不同"的校验必然通过 → 服务在随机端口"成功"启动,网关/探针配置全部失效,随机端口还可能恰好被占用而启动失败。

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:15ecmallServices 一致性断言,pkgs/all 两者都没有(只有 5.2 提到的非空校验)。
internal/server/{server,authorization,dynamic,response}.go 与 ecmall 的两份拷贝必须手工同步diff 结果这 4 个文件逐字节相同(唯一差异是 server.go:79-80 的日志字面量 all/ecmallecmallinternal/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:36module/base/logs/cmd/main/main.go:39module/base/mgt/cmd/main/main.go:41),聚合只有三个模块自带的 /rest/*/pingHEAD / 会落到 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.ModeSDK 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 的 printergin.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. 补生产配置与构建覆盖A1pkgs/all/etc/ 增加 default_prod.yaml(至少替换全部 CHANGE_ME、明确 BindIP),并把 scripts/build-all-linux.shMODULE_ROOT 扩展到 pkgs/(或额外的 PKGS_ROOT 循环),同时补 pkgs/all/etc/default_prod.yaml 的拷贝分支。
  2. 让占位密钥不能启动A2、A3internal/config/config.go 现有的 Authorization.Key 校验旁,加两条最小校验——Authorization.Key 不得等于样例字面量、SecretKey 非空且不得等于 CHANGE_ME(或直接要求 32 字节)。这是 4 行代码级别的改动,不要引入配置加密框架。
  3. 修 404 分流A4让 gateway 用独立标记区分"路由未命中"。最小做法是自定义 gwRuntime.ServeMuxWithRoutingErrorHandler(或在 bufferedResponse 上记录"是否由 DefaultRoutingErrorHandler 写出"),只在 codes.NotFound 路由未命中时才回退 gin其余情况直接 flush
  4. 关掉或收紧 reflectionA5Server 增加一个 Authorization.Reflection bool(默认 false仅在为 true 时 reflection.Register;同时从 isAnonymous 中删除 /grpc.health. 前缀,等真有 health 服务时再显式加白名单。注意动态 RPC 依赖反射,关闭前需确认该入口是否保留——若保留,则应改为"仅允许 loopback 调用动态入口"。
  5. 客户端 IP 与头部清理A7server.Newengine.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、A17Start 里任一 Serve 返回错误时关闭两个 listener 并 ShutdownStop 里给 http.Shutdown 单独一个未过期的短超时 ctx并把 main.go:52 的错误打印出来;config.New 增加 Databases 非空校验;Port 为空时改为报错退出而不是随机端口。
  9. 入口限流A8仅在 httpMiddleware 内对匿名路径加一个基于内存的简单计数/令牌桶(按路径 + 客户端 IP不引入外部依赖不要为它建框架。
  10. 加固两份拷贝与测试护栏A16pkgs/all 补一个与 pkgs/ecmall/internal/service/service_test.go 等价的注册表测试,并在 internal/config/config_test.go 中断言 Services 与注册表逐项一致;两份 internal/server 拷贝的同步问题,用"两处测试用例内容一致"来兜(不加抽取层)。
  11. 补齐入口可观测与一致性A13、A17 的其余项):http.ServerWriteTimeoutengine.SetModeBSM_RuntimeMode 设置;注册 HEAD / 健康端点;如需 CORS 再显式启用(当前缺失本身不算缺陷,但需与独立入口行为对齐并明确决策)。
  12. 流式方法的前置约束A10grpc.NewServer 处补 grpc.ChainStreamInterceptor,与 unary 共用同一份 validate;若短期内不打算支持流式,也在代码中写明"新增流式方法前必须先加拦截器"。

本报告只列与现有实现直接相关的修复项,不引入新的分层或抽象封装。审计中"把两个聚合入口的 server 抽成公共包""给动态 RPC 引入请求 DTO 层""用服务网格解决 mTLS"一类改造不在此列。