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

81 lines
5.5 KiB
Markdown

---
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 标注「已确认由管理后台同步,仅文档标注」
```