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