-
Notifications
You must be signed in to change notification settings - Fork 11.2k
安全修复:加固初始化、OAuth 绑定鉴权、自动更新语支付回调校验 #4504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,9 +15,11 @@ import ( | |
| "github.com/QuantumNous/new-api/setting" | ||
| "io" | ||
| "net/http" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| "github.com/shopspring/decimal" | ||
| "github.com/thanhpk/randstr" | ||
| ) | ||
|
|
||
|
|
@@ -337,6 +339,42 @@ func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) { | |
|
|
||
| // 处理充值,传入客户邮箱和姓名信息 | ||
| customerEmail := event.Object.Customer.Email | ||
| if topUp.PaymentProvider != model.PaymentProviderCreem { | ||
| logger.LogWarn(c.Request.Context(), fmt.Sprintf("Creem callback rejected provider=%s trade_no=%s reason=provider_mismatch actual_provider=%s client_ip=%s", model.PaymentProviderCreem, referenceId, topUp.PaymentProvider, c.ClientIP())) | ||
| c.AbortWithStatus(http.StatusBadRequest) | ||
| return | ||
| } | ||
| expectedAmount := decimal.NewFromFloat(topUp.Money).Round(2) | ||
| actualAmount := decimal.NewFromInt(int64(event.Object.Order.AmountPaid)).Div(decimal.NewFromInt(100)).Round(2) | ||
| if !expectedAmount.Equal(actualAmount) { | ||
| logPaymentReject(c.Request.Context(), model.PaymentProviderCreem, referenceId, expectedAmount, actualAmount, "amount_mismatch", c.ClientIP()) | ||
| c.AbortWithStatus(http.StatusBadRequest) | ||
| return | ||
| } | ||
| var products []CreemProduct | ||
| if err := common.UnmarshalJsonStr(setting.CreemProducts, &products); err == nil { | ||
| var matchedProducts []CreemProduct | ||
| for _, product := range products { | ||
| if product.Quota == topUp.Amount && decimal.NewFromFloat(product.Price).Round(2).Equal(expectedAmount) { | ||
| matchedProducts = append(matchedProducts, product) | ||
| } | ||
| } | ||
| if len(matchedProducts) == 1 { | ||
| expectedProduct := matchedProducts[0] | ||
| if expectedProduct.ProductId != "" && event.Object.Product.Id != expectedProduct.ProductId { | ||
| logger.LogWarn(c.Request.Context(), fmt.Sprintf("Creem callback rejected provider=%s trade_no=%s reason=product_mismatch expected_product=%s actual_product=%s client_ip=%s", model.PaymentProviderCreem, referenceId, expectedProduct.ProductId, event.Object.Product.Id, c.ClientIP())) | ||
| c.AbortWithStatus(http.StatusBadRequest) | ||
| return | ||
| } | ||
| expectedCurrency := strings.ToUpper(expectedProduct.Currency) | ||
| actualCurrency := strings.ToUpper(event.Object.Order.Currency) | ||
| if expectedCurrency != "" && actualCurrency != expectedCurrency { | ||
| logger.LogWarn(c.Request.Context(), fmt.Sprintf("Creem callback rejected provider=%s trade_no=%s reason=currency_mismatch expected_currency=%s actual_currency=%s client_ip=%s", model.PaymentProviderCreem, referenceId, expectedCurrency, actualCurrency, c.ClientIP())) | ||
| c.AbortWithStatus(http.StatusBadRequest) | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+354
to
+377
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not silently skip product/currency validation when the config is ambiguous or unreadable. If |
||
| customerName := event.Object.Customer.Name | ||
|
|
||
| // 防护性检查,确保邮箱和姓名不为空字符串 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import ( | |
| "github.com/QuantumNous/new-api/setting/system_setting" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| "github.com/shopspring/decimal" | ||
| "github.com/stripe/stripe-go/v81" | ||
| "github.com/stripe/stripe-go/v81/checkout/session" | ||
| "github.com/stripe/stripe-go/v81/webhook" | ||
|
|
@@ -279,6 +280,38 @@ func fulfillOrder(ctx context.Context, event stripe.Event, referenceId string, c | |
| return | ||
| } | ||
|
|
||
| topUp := model.GetTopUpByTradeNo(referenceId) | ||
| if topUp == nil { | ||
| logger.LogWarn(ctx, fmt.Sprintf("Stripe callback rejected provider=%s trade_no=%s reason=order_not_found client_ip=%s", model.PaymentProviderStripe, referenceId, callerIp)) | ||
| return | ||
| } | ||
| if topUp.PaymentProvider != model.PaymentProviderStripe { | ||
| logger.LogWarn(ctx, fmt.Sprintf("Stripe callback rejected provider=%s trade_no=%s reason=provider_mismatch actual_provider=%s client_ip=%s", model.PaymentProviderStripe, referenceId, topUp.PaymentProvider, callerIp)) | ||
| return | ||
| } | ||
| if topUp.Status == common.TopUpStatusSuccess { | ||
| logger.LogInfo(ctx, fmt.Sprintf("Stripe callback idempotent success trade_no=%s client_ip=%s", referenceId, callerIp)) | ||
| return | ||
| } | ||
| if topUp.Status != common.TopUpStatusPending { | ||
| logger.LogWarn(ctx, fmt.Sprintf("Stripe callback rejected provider=%s trade_no=%s reason=invalid_status status=%s client_ip=%s", model.PaymentProviderStripe, referenceId, topUp.Status, callerIp)) | ||
| return | ||
| } | ||
| actualMinor, parseErr := strconv.ParseInt(event.GetObjectValue("amount_total"), 10, 64) | ||
| if parseErr != nil { | ||
| logger.LogWarn(ctx, fmt.Sprintf("Stripe callback rejected provider=%s trade_no=%s reason=invalid_amount_total client_ip=%s", model.PaymentProviderStripe, referenceId, callerIp)) | ||
| return | ||
| } | ||
| expectedAmount := decimal.NewFromFloat(topUp.Money).Round(2) | ||
| actualAmount := decimal.NewFromInt(actualMinor).Div(decimal.NewFromInt(100)).Round(2) | ||
| if !expectedAmount.Equal(actualAmount) { | ||
| logPaymentReject(ctx, model.PaymentProviderStripe, referenceId, expectedAmount, actualAmount, "amount_mismatch", callerIp) | ||
| return | ||
| } | ||
| if strings.ToUpper(event.GetObjectValue("currency")) != "USD" { | ||
| logger.LogWarn(ctx, fmt.Sprintf("Stripe callback rejected provider=%s trade_no=%s reason=currency_mismatch expected_currency=%s actual_currency=%s client_ip=%s", model.PaymentProviderStripe, referenceId, "USD", strings.ToUpper(event.GetObjectValue("currency")), callerIp)) | ||
| return | ||
| } | ||
|
Comment on lines
+283
to
+314
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
rg -n --type=go 'StripePriceId|StripeUnitPrice|Stripe.*Currency|PaymentProviderStripe|currency|USD' controller setting modelRepository: QuantumNous/new-api Length of output: 12683 Store or retrieve the expected Stripe currency per order instead of hardcoding USD. The webhook at line 311 rejects any non-USD currency, but 🤖 Prompt for AI Agents |
||
| err := model.Recharge(referenceId, customerId, callerIp) | ||
| if err != nil { | ||
| logger.LogError(ctx, fmt.Sprintf("Stripe 充值处理失败 trade_no=%s event_type=%s client_ip=%s error=%q", referenceId, string(event.Type), callerIp, err.Error())) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import ( | |
| "time" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/logger" | ||
| "github.com/QuantumNous/new-api/model" | ||
|
|
||
| "github.com/gin-contrib/sessions" | ||
|
|
@@ -104,6 +105,7 @@ func WeChatAuth(c *gin.Context) { | |
| return | ||
| } | ||
| } else { | ||
| logger.LogWarn(c.Request.Context(), fmt.Sprintf("wechat auto register rejected provider=wechat reason=register_disabled wechat_id=%s client_ip=%s", wechatId, c.ClientIP())) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging raw This is a stable external account identifier, so writing it verbatim to warn logs increases PII exposure. Prefer a hash/truncated form or log only internal request context. 🤖 Prompt for AI Agents |
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": "管理员关闭了新用户注册", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not acknowledge Epay before the recharge path succeeds.
The handler writes
"success"before the locked update/quota-credit block runs. IftopUp.Update()orIncreaseUserQuota()fails afterward, the gateway has already been told the callback succeeded, so the paid order can remain uncredited with no retry signal.Also applies to: 420-455
🤖 Prompt for AI Agents