Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions controller/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -54,6 +55,7 @@ func GetSetup(c *gin.Context) {
func PostSetup(c *gin.Context) {
// Check if setup is already completed
if constant.Setup {
logger.LogWarn(c.Request.Context(), "setup rejected reason=setup_completed client_ip="+c.ClientIP())
c.JSON(200, gin.H{
"success": false,
"message": "系统已经初始化完成",
Expand All @@ -63,6 +65,14 @@ func PostSetup(c *gin.Context) {

// Check if root user already exists
rootExists := model.RootUserExists()
if rootExists {
logger.LogWarn(c.Request.Context(), "setup rejected reason=root_exists client_ip="+c.ClientIP())
c.JSON(200, gin.H{
"success": false,
"message": "系统已经初始化完成",
})
return
}

var req SetupRequest
err := c.ShouldBindJSON(&req)
Expand Down
56 changes: 56 additions & 0 deletions controller/topup.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package controller

import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -177,6 +179,30 @@ func getMinTopup() int64 {
return int64(minTopup)
}

func normalizeMoneyDecimalFromFloat(amount float64) decimal.Decimal {
return decimal.NewFromFloat(amount).Round(2)
}

func normalizeMoneyDecimalFromMinorUnits(amount int64) decimal.Decimal {
return decimal.NewFromInt(amount).Div(decimal.NewFromInt(100)).Round(2)
}

func normalizeMoneyDecimalFromString(amount string) (decimal.Decimal, error) {
trimmed := strings.TrimSpace(amount)
if trimmed == "" {
return decimal.Zero, fmt.Errorf("empty amount")
}
value, err := decimal.NewFromString(trimmed)
if err != nil {
return decimal.Zero, err
}
return value.Round(2), nil
}

func logPaymentReject(ctx context.Context, provider string, tradeNo string, expectedAmount decimal.Decimal, actualAmount decimal.Decimal, reason string, clientIP string) {
logger.LogWarn(ctx, fmt.Sprintf("%s callback rejected provider=%s trade_no=%s expected_amount=%s actual_amount=%s reason=%s client_ip=%s", provider, provider, tradeNo, expectedAmount.StringFixed(2), actualAmount.StringFixed(2), reason, clientIP))
}

func RequestEpay(c *gin.Context) {
var req EpayRequest
err := c.ShouldBindJSON(&req)
Expand Down Expand Up @@ -344,6 +370,36 @@ func EpayNotify(c *gin.Context) {
verifyInfo, err := client.Verify(params)
if err == nil && verifyInfo.VerifyStatus {
logger.LogInfo(c.Request.Context(), fmt.Sprintf("易支付 webhook 验签成功 trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q", verifyInfo.ServiceTradeNo, verifyInfo.Type, verifyInfo.TradeStatus, c.ClientIP(), common.GetJsonString(verifyInfo)))
if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo)
if topUp == nil {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("Epay callback rejected provider=%s trade_no=%s reason=order_not_found client_ip=%s", model.PaymentProviderEpay, verifyInfo.ServiceTradeNo, c.ClientIP()))
_, _ = c.Writer.Write([]byte("fail"))
return
}
if topUp.PaymentProvider != model.PaymentProviderEpay {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("Epay callback rejected provider=%s trade_no=%s reason=provider_mismatch actual_provider=%s client_ip=%s", model.PaymentProviderEpay, verifyInfo.ServiceTradeNo, topUp.PaymentProvider, c.ClientIP()))
_, _ = c.Writer.Write([]byte("fail"))
return
}
expectedAmount := normalizeMoneyDecimalFromFloat(topUp.Money)
actualAmount, amountErr := normalizeMoneyDecimalFromString(verifyInfo.Money)
if amountErr != nil {
logPaymentReject(c.Request.Context(), model.PaymentProviderEpay, verifyInfo.ServiceTradeNo, expectedAmount, decimal.Zero, "invalid_callback_amount", c.ClientIP())
_, _ = c.Writer.Write([]byte("fail"))
return
}
if !expectedAmount.Equal(actualAmount) {
logPaymentReject(c.Request.Context(), model.PaymentProviderEpay, verifyInfo.ServiceTradeNo, expectedAmount, actualAmount, "amount_mismatch", c.ClientIP())
_, _ = c.Writer.Write([]byte("fail"))
return
}
if topUp.Status != common.TopUpStatusPending && topUp.Status != common.TopUpStatusSuccess {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("Epay callback rejected provider=%s trade_no=%s reason=invalid_status status=%s client_ip=%s", model.PaymentProviderEpay, verifyInfo.ServiceTradeNo, topUp.Status, c.ClientIP()))
_, _ = c.Writer.Write([]byte("fail"))
return
}
}
_, err := c.Writer.Write([]byte("success"))
Comment on lines +373 to 403

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not acknowledge Epay before the recharge path succeeds.

The handler writes "success" before the locked update/quota-credit block runs. If topUp.Update() or IncreaseUserQuota() 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
Verify each finding against the current code and only fix it if needed.

In `@controller/topup.go` around lines 373 - 403, The handler currently writes
"success" before performing the critical post-payment operations, so move the
c.Writer.Write([]byte("success")) call to after the locked update/quota-credit
block (i.e., only after topUp.Update() and IncreaseUserQuota() succeed); if
either topUp.Update() or IncreaseUserQuota() fails, log the error (use
logger.LogError or existing logPaymentReject), write "fail" and return so the
gateway can retry. Locate code paths using GetTopUpByTradeNo,
normalizeMoneyDecimalFromFloat/FromString, logPaymentReject, and the subsequent
topUp.Update()/IncreaseUserQuota() calls and ensure acknowledgement is
conditional on their success. Ensure all early reject paths still write "fail"
immediately.

if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("易支付 webhook 响应写入失败 trade_no=%s client_ip=%s error=%q", verifyInfo.ServiceTradeNo, c.ClientIP(), err.Error()))
Expand Down
38 changes: 38 additions & 0 deletions controller/topup_creem.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not silently skip product/currency validation when the config is ambiguous or unreadable.

If CreemProducts fails to parse, or multiple configured products share the same (quota, price) pair, this branch falls through and accepts the callback on amount alone. That makes the new product/currency check best-effort instead of authoritative. Persist the selected product_id/currency with the local order and compare against those exact values here, or reject when the config cannot identify a unique match.

customerName := event.Object.Customer.Name

// 防护性检查,确保邮箱和姓名不为空字符串
Expand Down
33 changes: 33 additions & 0 deletions controller/topup_stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
rg -n --type=go 'StripePriceId|StripeUnitPrice|Stripe.*Currency|PaymentProviderStripe|currency|USD' controller setting model

Repository: 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 setting.StripePriceId (used in checkout creation at line 396) is configurable and can point to a Stripe Price in any currency. If a non-USD Price is configured, valid payments will be rejected. The codebase currently has no mechanism to persist or derive the expected currency; consider storing it with the order when the Stripe session is created.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_stripe.go` around lines 283 - 314, The webhook wrongly
hardcodes "USD" when validating currency; update the flow to store the expected
currency on the order when creating the Stripe checkout session (the code that
uses setting.StripePriceId) and then use that stored value in the callback
instead of "USD". Specifically: add/persist an expected currency field on the
TopUp record when creating the Stripe session (lookup the Price/currency or read
it from the created session in the checkout creation code), populate
topUp.Currency (or similar) there, and change the callback logic that currently
calls event.GetObjectValue("currency") and compares to "USD" to compare against
topUp.Currency (case-normalized) and log appropriately via logPaymentReject /
logger.LogWarn when mismatched. Ensure GetTopUpByTradeNo, topUp.PaymentProvider,
topUp.Status, and the amount checks remain unchanged.

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()))
Expand Down
34 changes: 34 additions & 0 deletions controller/topup_waffo.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"github.com/thanhpk/randstr"
waffo "github.com/waffo-com/waffo-go"
"github.com/waffo-com/waffo-go/config"
Expand Down Expand Up @@ -394,6 +395,39 @@ func handleWaffoPayment(c *gin.Context, wh *core.WebhookHandler, result *core.Pa

LockOrder(merchantOrderId)
defer UnlockOrder(merchantOrderId)
topUp := model.GetTopUpByTradeNo(merchantOrderId)
if topUp == nil {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo callback rejected provider=%s trade_no=%s reason=order_not_found client_ip=%s", model.PaymentProviderWaffo, merchantOrderId, c.ClientIP()))
sendWaffoWebhookResponse(c, wh, false, "order not found")
return
}
if topUp.PaymentProvider != model.PaymentProviderWaffo {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo callback rejected provider=%s trade_no=%s reason=provider_mismatch actual_provider=%s client_ip=%s", model.PaymentProviderWaffo, merchantOrderId, topUp.PaymentProvider, c.ClientIP()))
sendWaffoWebhookResponse(c, wh, false, "provider mismatch")
return
}
if topUp.Status == common.TopUpStatusSuccess {
logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo callback idempotent success trade_no=%s client_ip=%s", merchantOrderId, c.ClientIP()))
sendWaffoWebhookResponse(c, wh, true, "")
return
}
if topUp.Status != common.TopUpStatusPending {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo callback rejected provider=%s trade_no=%s reason=invalid_status status=%s client_ip=%s", model.PaymentProviderWaffo, merchantOrderId, topUp.Status, c.ClientIP()))
sendWaffoWebhookResponse(c, wh, false, "invalid status")
return
}
expectedAmount := decimal.NewFromFloat(topUp.Money).Round(2)
actualAmount, err := normalizeMoneyDecimalFromString(result.OrderAmount)
if err != nil {
logPaymentReject(c.Request.Context(), model.PaymentProviderWaffo, merchantOrderId, expectedAmount, decimal.Zero, "invalid_callback_amount", c.ClientIP())
sendWaffoWebhookResponse(c, wh, false, "invalid amount")
return
}
if !expectedAmount.Equal(actualAmount) {
logPaymentReject(c.Request.Context(), model.PaymentProviderWaffo, merchantOrderId, expectedAmount, actualAmount, "amount_mismatch", c.ClientIP())
sendWaffoWebhookResponse(c, wh, false, "amount mismatch")
return
}

if err := model.RechargeWaffo(merchantOrderId, c.ClientIP()); err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo 充值处理失败 trade_no=%s client_ip=%s error=%q", merchantOrderId, c.ClientIP(), err.Error()))
Expand Down
86 changes: 79 additions & 7 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,39 @@ func UpdateSelf(c *gin.Context) {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
sensitiveFields := []string{
"quota",
"balance",
"role",
"group",
"status",
"aff_code",
"used_quota",
"request_count",
"inviter_id",
"stripe_customer",
"access_token",
"token",
"root",
"is_admin",
"permission",
"quota_setting",
"subscription",
}
for _, field := range sensitiveFields {
if _, exists := requestData[field]; exists {
logger.LogWarn(c.Request.Context(), fmt.Sprintf("forbidden self update rejected user_id=%d field=%s reason=forbidden_self_update_field client_ip=%s", c.GetInt("id"), field, c.ClientIP()))
common.ApiErrorI18n(c, i18n.MsgInvalidInput)
return
}
}

// 检查是否是用户设置更新请求 (sidebar_modules 或 language)
if sidebarModules, sidebarExists := requestData["sidebar_modules"]; sidebarExists {
if len(requestData) != 1 {
common.ApiErrorI18n(c, i18n.MsgInvalidInput)
return
}
userId := c.GetInt("id")
user, err := model.GetUserById(userId, false)
if err != nil {
Expand Down Expand Up @@ -661,6 +691,10 @@ func UpdateSelf(c *gin.Context) {

// 检查是否是语言偏好更新请求
if language, langExists := requestData["language"]; langExists {
if len(requestData) != 1 {
common.ApiErrorI18n(c, i18n.MsgInvalidInput)
return
}
userId := c.GetInt("id")
user, err := model.GetUserById(userId, false)
if err != nil {
Expand Down Expand Up @@ -688,17 +722,55 @@ func UpdateSelf(c *gin.Context) {
}

// 原有的用户信息更新逻辑
var user model.User
requestDataBytes, err := json.Marshal(requestData)
if err != nil {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
allowedFields := map[string]struct{}{
"username": {},
"password": {},
"display_name": {},
"original_password": {},
}
for field := range requestData {
if _, ok := allowedFields[field]; !ok {
common.ApiErrorI18n(c, i18n.MsgInvalidInput)
return
}
}
err = json.Unmarshal(requestDataBytes, &user)
if err != nil {
if len(requestData) == 0 {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
var user model.User
if username, ok := requestData["username"]; ok {
usernameStr, ok := username.(string)
if !ok {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
user.Username = usernameStr
}
if displayName, ok := requestData["display_name"]; ok {
displayNameStr, ok := displayName.(string)
if !ok {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
user.DisplayName = displayNameStr
}
if password, ok := requestData["password"]; ok {
passwordStr, ok := password.(string)
if !ok {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
user.Password = passwordStr
}
if originalPassword, ok := requestData["original_password"]; ok {
originalPasswordStr, ok := originalPassword.(string)
if !ok {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
user.OriginalPassword = originalPasswordStr
}

if user.Password == "" {
user.Password = "$I_LOVE_U" // make Validator happy :)
Expand Down
2 changes: 2 additions & 0 deletions controller/wechat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging raw wechat_id.

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
Verify each finding against the current code and only fix it if needed.

In `@controller/wechat.go` at line 108, The warning log currently prints the raw
external account identifier wechatId in the logger.LogWarn call; update the code
that calls logger.LogWarn (the site using wechatId and c.ClientIP()) to avoid
logging raw PII by deriving and logging a non-reversible fingerprint (e.g., hash
like SHA256 and truncate to a short fixed length) or a truncated masked form of
wechatId instead, and keep c.ClientIP() as-is; ensure the new value replaces
wechatId in the fmt.Sprintf invocation so only the hashed/truncated/masked
identifier is written to logs.

c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员关闭了新用户注册",
Expand Down
Loading