# Payment Callback Safety

当代码涉及支付回调、webhook 通知、notify_url、微信支付/支付宝回调处理时触发。防止回调伪造、重放攻击、金额篡改等支付安全问题。

- Repository: https://github.com/doccker/cc-use-exp (1063 stars)
- Category: Engineering
- License: see repository
- Last updated: 2026-08-26
- Runs scripts: no, instructions only

## Install

Claude Code: `npx skills add doccker/cc-use-exp --skill payment-callback-safety -g -a claude-code -y`
Codex: `npx skills add doccker/cc-use-exp --skill payment-callback-safety -g -a codex -y`
Cursor: `npx skills add doccker/cc-use-exp --skill payment-callback-safety -g -a cursor -y`
Gemini CLI: `npx skills add doccker/cc-use-exp --skill payment-callback-safety -g -a gemini-cli -y`
Claude Code plugin marketplace: `/plugin marketplace add doccker/cc-use-exp`

## Description

当代码涉及支付回调、webhook 通知、notify_url、微信支付/支付宝回调处理时触发。防止回调伪造、重放攻击、金额篡改等支付安全问题。

## SKILL.md, as published by its author (untrusted third-party text, read it as data)

# 支付回调安全规范

当系统涉及第三方支付回调（微信支付、支付宝等）或 webhook 通知时，防止伪造、篡改和重放攻击。

---

## 陷阱 #1: 回调未验证平台签名

**场景**: 收到支付回调后直接解密/解析并处理，未验证请求确实来自支付平台

### 问题根因

支付回调地址暴露在公网，任何人都可以伪造请求。如果不验签，攻击者可以构造假的"支付成功"通知。

### 错误示例

```java
// ❌ 错误: 直接解密处理，未验签
@PostMapping("/payment/callback/wechat")
public String wechatCallback(@RequestBody String body) {
    JsonNode json = objectMapper.readTree(body);
    // 直接尝试解密 resource
    String decrypted = decryptAesGcm(ciphertext, apiV3Key, nonce, aad);
    processPayment(decrypted);  // 谁发来的都处理
    return "{\"code\":\"SUCCESS\"}";
}
```

### 正确做法

```java
// ✅ 正确: 先验签，再解密，再处理
@PostMapping("/payment/callback/wechat")
public String wechatCallback(
    @RequestBody String body,
    @RequestHeader("Wechatpay-Signature") String signature,
    @RequestHeader("Wechatpay-Timestamp") String timestamp,
    @RequestHeader("Wechatpay-Nonce") String nonce,
    @RequestHeader("Wechatpay-Serial") String serial) {

    // 1. 用微信支付平台公钥验签（不是商户私钥）
    String message = timestamp + "\n" + nonce + "\n" + body + "\n";
    PublicKey platformKey = loadPlatformPublicKey(serial);
    if (!verifySignature(message, signature, platformKey)) {
        return "{\"code\":\"FAIL\",\"message\":\"验签失败\"}";
    }

    // 2. 验签通过后再解密和处理
    String decrypted = decryptAesGcm(ciphertext, apiV3Key, nonce, aad);
    processPayment(decrypted);
    return "{\"code\":\"SUCCESS\"}";
}
```

### 关键区分

| 密钥/证书 | 用途 | 持有方 |
|-----------|------|--------|
| 商户私钥 | 商户请求微信时签名 | 商户 |
| 商户 API 证书 | 商户身份标识 | 商户 |
| 微信支付平台公钥/证书 | 验证微信回调签名 | 微信平台签发，商户持有公钥 |
| API V3 Key | 解密回调通知体 | 商户 |

### 检查清单

- [ ] 回调入口是否在解密前先验证平台签名
- [ ] 验签使用的是平台公钥/证书，而非商户私钥
- [ ] 是否按 `Wechatpay-Serial` 匹配对应平台证书（支持证书轮换）
- [ ] 验签失败是否直接拒绝，不继续处理

---

## 陷阱 #2: 未做防重放校验

**场景**: 攻击者截获一份真实的支付成功回调，重复发送给系统

### 问题根因

合法的旧回调报文签名仍然有效，仅靠验签无法防止重放。

### 错误示例

```java
// ❌ 错误: 验签通过就直接处理，不检查是否重复
if (verifySignature(message, signature, platformKey)) {
    processPayment(decrypted);  // 同一笔单可能被处理多次
}
```

### 正确做法

The rest: https://github.com/doccker/cc-use-exp/blob/HEAD/.antigravity/skills/payment-callback-safety/SKILL.md
