From d2f7f9ee3adf3ef66798783a60d7bc712451c85c Mon Sep 17 00:00:00 2001 From: Seefs <40468931+seefs001@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:39:29 +0800 Subject: [PATCH] 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 --- common/init.go | 1 + common/request_body_limit.go | 13 +++++++++ constant/env.go | 1 + middleware/request_body_limit.go | 47 ++++++++++++++++++++++++++++++++ router/api-router.go | 33 +++++++++++----------- 5 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 common/request_body_limit.go create mode 100644 middleware/request_body_limit.go diff --git a/common/init.go b/common/init.go index 138bc8ff..6b9fca83 100644 --- a/common/init.go +++ b/common/init.go @@ -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) diff --git a/common/request_body_limit.go b/common/request_body_limit.go new file mode 100644 index 00000000..8579ae9f --- /dev/null +++ b/common/request_body_limit.go @@ -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 +} diff --git a/constant/env.go b/constant/env.go index d5aff1b0..512bfc31 100644 --- a/constant/env.go +++ b/constant/env.go @@ -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 diff --git a/middleware/request_body_limit.go b/middleware/request_body_limit.go new file mode 100644 index 00000000..3be0d901 --- /dev/null +++ b/middleware/request_body_limit.go @@ -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 +} diff --git a/router/api-router.go b/router/api-router.go index 381d2ccd..e98dc66a 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -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()) {