fix: 按评审报告止血支付漏洞并加固授权链路
废除浏览器回跳自助结算,通用 webhook 强制密钥鉴权,Waffo 支持验签与未知事件忽略;重构订单状态机/激活占座/SQLite WAL,并补备份、readyz、限流与前端导出/登录页安全修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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("不支持的公钥格式")
|
||||
}
|
||||
Reference in new issue
Block a user