Files
zt-closet-backend/.codebuddy/plans/修复XPay发货推送伪造漏洞_a31999c1(未完成).md
T
2026-09-11 01:41:52 +08:00

5.5 KiB

name, overview, todos
name overview todos
修复XPay发货推送伪造漏洞 修复 S1:XPay 发货推送在条件流转发放权益前增加微信侧订单真实性反查(QueryOrder),杜绝伪造推送零成本获取权益;S2 不做代码修复,仅在 README 标注道具价格同步由管理后台负责的运维约束。
id content status
add-paid-status-helper 在 pkg/wechat/virtualpay.go 新增「已支付」状态常量与 IsOrderPaidStatus 白名单判断函数,并在 virtualpay_test.go 补充单测(含未知状态、空串) pending
id content status dependencies
add-reality-check 在 pkg/errcode/order_code.go 新增 70009 错误码,并在 internal/service/virtualpay.go 的 handleGoodsDeliver 中本地校验后、条件流转前插入 QueryOrder 反查(fail-closed,openid 为空/接口错误/未支付/金额道具不符均返回错误让微信重试,附审计日志) pending
add-paid-status-helper
id content status
update-docs 更新 README.md 标注道具同步运营约束(管理后台负责、改价必须同步及其风险),并同步更新 docs/代码审查报告-二次check.md 中 S1「已修复」、S2「已确认由管理后台同步,仅文档标注」 pending

用户需求

  1. 修复 S1(资损漏洞):XPay 发货推送无验签,攻击者可伪造 xpay_goods_deliver_notify 推送零成本获取权益。按已确认方案修复:发货前调用带 HMAC-SHA256 签名的微信服务端 API query_order 反查订单真实支付状态,确认「已支付 + 金额一致 + 道具一致」后才流转发放,推送仅作触发信号。
  2. S2 仅文档标注:不做道具周期同步代码。用户有自己的管理后台负责道具同步,仅在 README 标注运营约束:运营改价后必须通过管理后台同步道具(或重启服务),否则微信侧道具价格与本地不一致,会导致发货金额校验拒绝(用户付款拿不到权益)或差价资损。

范围边界

  • 仅修改虚拟支付链路:发货推送处理(handleGoodsDeliver)增加真实性反查;普通微信支付 V3 回调链路(官方 SDK 验签)不涉及
  • 退款推送伪造风险与 M1(RefundStatus 空串误判)不在本次范围
  • 交付物:修复后的代码 + 单测 + README 与审查报告文档更新

技术方案

实现策略

在 handleGoodsDeliver 中,幂等锁内、全部本地校验(pay_channel / product_id / amount / env)通过之后、条件流转(Pending→Paid)之前,插入微信侧订单真实性反查。反查失败或结果不符时返回非 nil 错误码,使 XPayNotify 回 ErrCode:1,微信按推送重试策略重试(fail-closed)。

关键设计

1. 支付状态白名单判断(pkg/wechat 层,与微信枚举集中管理)

  • 在 pkg/wechat/virtualpay.go 新增导出函数 IsOrderPaidStatus(status string) bool 与状态常量(如 xpayOrderStatusPaid,枚举值以官方文档为准,联调校正)
  • 未知状态按「未支付」处理并告警,保证 fail-closed
  • internal/service/virtualpay.go 调用该函数,不散落魔法字符串

2. 反查校验函数(service 层)

  • 新增 verifyXPayOrderPaid(ctx, order, deliver) *errcode.Error:
  • deliver.OpenID 为空 → 直接拒绝(返回新错误码 70009),审计日志
  • 调用 wechat.QueryOrder(ctx, deliver.OpenID, deliver.OutTradeNo, "")
  • 接口错误 → 返回非 nil 错误(fail-closed),日志携带 out_trade_no 与 error
  • !wechat.IsOrderPaidStatus(info.Status) → 拒绝 + 日志(含 query 返回状态)
  • info.PayFee != order.PayAmount → 拒绝 + 日志(金额不符,资损线索)
  • info.ProductID != order.XPayProductID → 拒绝 + 日志(道具不符)
  • 全部通过 → 返回 nil,继续原有条件流转与 finalizeVirtualPaidOrder 发货
  • 插入位置:handleGoodsDeliver 中 env 校验之后、OrderUpdateStatusByOrderNo(Pending→Paid) 之前

3. 错误码

  • pkg/errcode/order_code.go 新增 OrderRealityCheckFailed = NewError(70009, "订单真实性校验失败"),语义用于「反查未通过/异常」,触发微信重试

4. 安全性与一致性

  • 反查走 callXPay 的 HMAC 签名(激活环境 AppKey),攻击者无法伪造;伪造推送在微信侧无支付记录,反查必然失败 → 拒绝发货
  • 真实支付后微信必然推送且反查必可查到已支付,重试机制保证最终发货
  • 反查使用激活环境(现网/沙箱),与订单 XPayEnv 快照已通过本地校验一致
  • 成功路径沿用现有 finalizeVirtualPaidOrder(callback_raw 持久化原始推送),审计闭环不变

性能与稳定性

  • 反查仅在幂等锁内执行一次(每笔发货推送一次 HTTP 调用,10s 超时),不引入额外热点
  • 沙箱环境 query_order 行为需联调确认(代码注释保持与现有「以官方文档为准」风格一致)

文件修改清单

zt-closet-backend/
├── pkg/wechat/virtualpay.go          # [MODIFY] 新增 XPay 订单已支付状态常量与 IsOrderPaidStatus 白名单判断函数
├── pkg/wechat/virtualpay_test.go     # [MODIFY] 补充 IsOrderPaidStatus 单测:已支付/未支付/未知状态/空串
├── pkg/errcode/order_code.go         # [MODIFY] 新增 70009 订单真实性校验失败错误码
├── internal/service/virtualpay.go    # [MODIFY] handleGoodsDeliver 插入 verifyXPayOrderPaid 反查(fail-closed)+审计日志
├── README.md                         # [MODIFY] S2 标注:道具同步由管理后台负责,改价必须同步(风险说明)
└── docs/代码审查报告-二次check.md      # [MODIFY] S1 标注「已修复」,S2 标注「已确认由管理后台同步,仅文档标注」