mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-04 16:42:20 +00:00
fix: limit anonymous request body (#5244)
* fix: limit anonymous request body (env ANONYMOUS_REQUEST_BODY_LIMIT_KB = 512) * fix: allow disabling anonymous request body limit
This commit is contained in:
@@ -136,6 +136,7 @@ func initConstantEnv() {
|
||||
constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 128)
|
||||
// MaxRequestBodyMB 请求体最大大小(解压后),用于防止超大请求/zip bomb导致内存暴涨
|
||||
constant.MaxRequestBodyMB = GetEnvOrDefault("MAX_REQUEST_BODY_MB", 128)
|
||||
constant.AnonymousRequestBodyLimitKB = GetEnvOrDefault("ANONYMOUS_REQUEST_BODY_LIMIT_KB", 512)
|
||||
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
||||
constant.ForceStreamOption = GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true)
|
||||
constant.CountToken = GetEnvOrDefaultBool("CountToken", true)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package common
|
||||
|
||||
import "github.com/QuantumNous/new-api/constant"
|
||||
|
||||
const defaultAnonymousRequestBodyLimitKB = 512
|
||||
|
||||
func GetAnonymousRequestBodyLimitBytes() int64 {
|
||||
limitKB := constant.AnonymousRequestBodyLimitKB
|
||||
if limitKB < 0 {
|
||||
limitKB = defaultAnonymousRequestBodyLimitKB
|
||||
}
|
||||
return int64(limitKB) << 10
|
||||
}
|
||||
@@ -10,6 +10,7 @@ var GetMediaToken bool
|
||||
var GetMediaTokenNotStream bool
|
||||
var UpdateTask bool
|
||||
var MaxRequestBodyMB int
|
||||
var AnonymousRequestBodyLimitKB int
|
||||
var AzureDefaultAPIVersion string
|
||||
var NotifyLimitCount int
|
||||
var NotificationLimitDurationMinute int
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AnonymousRequestBodyLimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
maxBytes := common.GetAnonymousRequestBodyLimitBytes()
|
||||
if maxBytes <= 0 || c.Request.Body == nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
originalBody := c.Request.Body
|
||||
limitedBody, err := readAnonymousRequestBody(originalBody, maxBytes)
|
||||
_ = originalBody.Close()
|
||||
if err != nil {
|
||||
if common.IsRequestBodyTooLargeError(err) {
|
||||
c.AbortWithStatus(http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(limitedBody))
|
||||
c.Request.ContentLength = int64(len(limitedBody))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func readAnonymousRequestBody(body io.Reader, maxBytes int64) ([]byte, error) {
|
||||
data, err := io.ReadAll(io.LimitReader(body, maxBytes+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(data)) > maxBytes {
|
||||
return nil, common.ErrRequestBodyTooLarge
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
+17
-16
@@ -17,9 +17,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
apiRouter.Use(middleware.BodyStorageCleanup()) // 清理请求体存储
|
||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||
anonymousRequestBodyLimit := middleware.AnonymousRequestBodyLimit()
|
||||
{
|
||||
apiRouter.GET("/setup", controller.GetSetup)
|
||||
apiRouter.POST("/setup", controller.PostSetup)
|
||||
apiRouter.POST("/setup", anonymousRequestBodyLimit, controller.PostSetup)
|
||||
apiRouter.GET("/status", controller.GetStatus)
|
||||
apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus)
|
||||
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
|
||||
@@ -40,39 +41,39 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.GET("/rankings", middleware.HeaderNavModuleAuth("rankings"), controller.GetRankings)
|
||||
apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.ResetPassword)
|
||||
// OAuth routes - specific routes must come before :provider wildcard
|
||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||
apiRouter.POST("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
apiRouter.POST("/oauth/email/bind", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.EmailBind)
|
||||
// Non-standard OAuth (WeChat, Telegram) - keep original routes
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||
apiRouter.POST("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.POST("/oauth/wechat/bind", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
|
||||
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
|
||||
apiRouter.GET("/oauth/:provider", middleware.CriticalRateLimit(), controller.HandleOAuth)
|
||||
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
|
||||
|
||||
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
||||
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
|
||||
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
|
||||
apiRouter.POST("/stripe/webhook", anonymousRequestBodyLimit, controller.StripeWebhook)
|
||||
apiRouter.POST("/creem/webhook", anonymousRequestBodyLimit, controller.CreemWebhook)
|
||||
apiRouter.POST("/waffo/webhook", anonymousRequestBodyLimit, controller.WaffoWebhook)
|
||||
// :env separates test vs prod URLs so the operator can register each
|
||||
// in Pancake's matching webhook slot; handler enforces env match.
|
||||
apiRouter.POST("/waffo-pancake/webhook/:env", controller.WaffoPancakeWebhook)
|
||||
apiRouter.POST("/waffo-pancake/webhook/:env", anonymousRequestBodyLimit, controller.WaffoPancakeWebhook)
|
||||
|
||||
// Universal secure verification routes
|
||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
||||
|
||||
userRoute := apiRouter.Group("/user")
|
||||
{
|
||||
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
||||
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
|
||||
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), controller.Verify2FALogin)
|
||||
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), controller.PasskeyLoginBegin)
|
||||
userRoute.POST("/passkey/login/finish", middleware.CriticalRateLimit(), controller.PasskeyLoginFinish)
|
||||
userRoute.POST("/register", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Register)
|
||||
userRoute.POST("/login", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Login)
|
||||
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.Verify2FALogin)
|
||||
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.PasskeyLoginBegin)
|
||||
userRoute.POST("/passkey/login/finish", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.PasskeyLoginFinish)
|
||||
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
|
||||
userRoute.GET("/logout", controller.Logout)
|
||||
userRoute.POST("/epay/notify", controller.EpayNotify)
|
||||
userRoute.POST("/epay/notify", anonymousRequestBodyLimit, controller.EpayNotify)
|
||||
userRoute.GET("/epay/notify", controller.EpayNotify)
|
||||
userRoute.GET("/groups", controller.GetUserGroups)
|
||||
|
||||
@@ -176,10 +177,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
}
|
||||
|
||||
// Subscription payment callbacks (no auth)
|
||||
apiRouter.POST("/subscription/epay/notify", controller.SubscriptionEpayNotify)
|
||||
apiRouter.POST("/subscription/epay/notify", anonymousRequestBodyLimit, controller.SubscriptionEpayNotify)
|
||||
apiRouter.GET("/subscription/epay/notify", controller.SubscriptionEpayNotify)
|
||||
apiRouter.GET("/subscription/epay/return", controller.SubscriptionEpayReturn)
|
||||
apiRouter.POST("/subscription/epay/return", controller.SubscriptionEpayReturn)
|
||||
apiRouter.POST("/subscription/epay/return", anonymousRequestBodyLimit, controller.SubscriptionEpayReturn)
|
||||
optionRoute := apiRouter.Group("/option")
|
||||
optionRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user