fix: 按评审报告止血支付漏洞并加固授权链路

废除浏览器回跳自助结算,通用 webhook 强制密钥鉴权,Waffo 支持验签与未知事件忽略;重构订单状态机/激活占座/SQLite WAL,并补备份、readyz、限流与前端导出/登录页安全修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 21:26:58 +08:00
1 parent 8cc2c83f9d
commit bac72e6945
24 files changed
+682 -391

No files matched your search

@@ -0,0 +1,127 @@
package payment
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"backend/internal/model"
"gorm.io/gorm"
)
// LoadWaffoWebhookVerifyKey 读取 webhook 验签材料:环境变量优先,其次网关 WebhookSecret // Logan.Li
func LoadWaffoWebhookVerifyKey(db *gorm.DB) string {
if v := strings.TrimSpace(os.Getenv("WAFFO_WEBHOOK_PUBLIC_KEY")); v != "" {
return v
}
if v := strings.TrimSpace(os.Getenv("WAFFO_WEBHOOK_SECRET")); v != "" {
return v
}
if db == nil {
return ""
}
var gw model.PaymentGatewayConfig
if err := db.Where("gateway = ?", "waffo").First(&gw).Error; err != nil {
return ""
}
return strings.TrimSpace(gw.WebhookSecret)
}
// VerifyWaffoWebhookSignature 校验 Waffo webhook:RSA PKCS1v15(SHA256(body)) 或 HMAC 共享密钥 // Logan.Li
func VerifyWaffoWebhookSignature(verifyKey string, body []byte, sigHeader string) error {
verifyKey = strings.TrimSpace(verifyKey)
sigHeader = strings.TrimSpace(sigHeader)
if verifyKey == "" {
return errors.New("未配置 webhook 验签公钥/密钥")
}
if sigHeader == "" {
return errors.New("缺少 X-Waffo-Signature")
}
// 共享密钥模式:签名头即为密钥(或 Bearer 同值) // Logan.Li
if !looksLikePEMOrDER(verifyKey) {
secret := strings.TrimPrefix(sigHeader, "Bearer ")
if subtleConstantTimeEq(secret, verifyKey) || subtleConstantTimeEq(sigHeader, verifyKey) {
return nil
}
return errors.New("webhook 共享密钥不匹配")
}
pub, err := parseRSAPublicKey(verifyKey)
if err != nil {
return fmt.Errorf("webhook 验签公钥无效: %w", err)
}
sig, err := base64.StdEncoding.DecodeString(sigHeader)
if err != nil {
// 兼容 URL-safe base64 // Logan.Li
sig, err = base64.RawURLEncoding.DecodeString(sigHeader)
if err != nil {
return errors.New("签名格式错误")
}
}
sum := sha256.Sum256(body)
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig); err != nil {
return errors.New("webhook 签名校验失败")
}
return nil
}
func looksLikePEMOrDER(s string) bool {
return strings.Contains(s, "BEGIN") || len(s) > 80
}
func subtleConstantTimeEq(a, b string) bool {
if len(a) != len(b) {
return false
}
var v byte
for i := 0; i < len(a); i++ {
v |= a[i] ^ b[i]
}
return v == 0
}
func parseRSAPublicKey(raw string) (*rsa.PublicKey, error) {
raw = strings.TrimSpace(raw)
raw = strings.ReplaceAll(raw, `\n`, "\n")
if block, _ := pem.Decode([]byte(raw)); block != nil {
return parseRSAPublicKeyBytes(block.Bytes)
}
compact := strings.Map(func(r rune) rune {
if r == '\n' || r == '\r' || r == ' ' || r == '\t' {
return -1
}
return r
}, raw)
if decoded, err := base64.StdEncoding.DecodeString(compact); err == nil {
if pub, err2 := parseRSAPublicKeyBytes(decoded); err2 == nil {
return pub, nil
}
}
wrapped := "-----BEGIN PUBLIC KEY-----\n" + chunkBase64(compact, 64) + "\n-----END PUBLIC KEY-----\n"
if block, _ := pem.Decode([]byte(wrapped)); block != nil {
return parseRSAPublicKeyBytes(block.Bytes)
}
return nil, errors.New("无法解析 RSA 公钥")
}
func parseRSAPublicKeyBytes(der []byte) (*rsa.PublicKey, error) {
if pub, err := x509.ParsePKIXPublicKey(der); err == nil {
if rsaPub, ok := pub.(*rsa.PublicKey); ok {
return rsaPub, nil
}
return nil, errors.New("公钥不是 RSA")
}
if pub, err := x509.ParsePKCS1PublicKey(der); err == nil {
return pub, nil
}
return nil, errors.New("不支持的公钥格式")
}