fix(middleware): log rejected CSRF requests with the trusted client address (#6692)

A panel or API request rejected for a missing or wrong CSRF token got a bare
403 and left nothing in the log, so an operator chasing failed logins from a
stale tab or a script had no trace. CSRFMiddleware now logs a warning with the
method, path and client address. The address comes from getRemoteIp, which
honours the trusted-proxy list, instead of gin's ClientIP, which trusts any
peer's X-Forwarded-For.
This commit is contained in:
ericanannn-blip
2026-10-24 10:51:21 +08:00
committed by GitHub
parent 864cbd234a
commit 3051aebb28
6 changed files with 53 additions and 12 deletions
+1 -1
View File
@@ -198,7 +198,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
// Decode + verify the node config envelope (zstd + X-Config-Sha256) and
// advertise support, before CSRF/handlers read the body.
api.Use(middleware.ConfigEnvelopeMiddleware())
api.Use(middleware.CSRFMiddleware())
api.Use(middleware.CSRFMiddleware(getRemoteIp))
api.GET("/openapi.json", ServeOpenAPISpec)
+41
View File
@@ -0,0 +1,41 @@
package controller
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
)
func TestLoginCSRFRejectionLogsPeerIPNotForwardedHeader(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.Use(sessions.Sessions("3x-ui", cookie.NewStore([]byte("csrf-log-test-secret"))))
NewIndexController(engine.Group("/"))
req := httptest.NewRequest(http.MethodPost, "/login", nil)
req.RemoteAddr = "198.51.100.9:40000"
req.Header.Set("X-Forwarded-For", "203.0.113.66")
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
}
var line string
for _, l := range logger.GetLogs(50, "WARNING") {
if strings.Contains(l, "CSRF validation failed") {
line = l
break
}
}
if !strings.Contains(line, "IP=198.51.100.9") || strings.Contains(line, "203.0.113.66") {
t.Fatalf("CSRF rejection log = %q, want the untrusted peer 198.51.100.9 and not the forwarded header", line)
}
}
+3 -3
View File
@@ -47,9 +47,9 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/sponsors", a.sponsors)
g.GET("/sponsors/logo/:name", a.sponsorLogo)
g.POST("/login", middleware.CSRFMiddleware(), a.login)
g.POST("/logout", middleware.CSRFMiddleware(), a.logout)
g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
g.POST("/login", middleware.CSRFMiddleware(getRemoteIp), a.login)
g.POST("/logout", middleware.CSRFMiddleware(getRemoteIp), a.logout)
g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(getRemoteIp), a.getTwoFactorEnable)
}
// sponsors is public so the login page can render its slot; failures stay silent.
+1 -1
View File
@@ -33,7 +33,7 @@ func NewXUIController(g *gin.RouterGroup) *XUIController {
func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel")
g.Use(a.checkLogin)
g.Use(middleware.CSRFMiddleware())
g.Use(middleware.CSRFMiddleware(getRemoteIp))
g.GET("/", a.panelSPA)
g.GET("/inbounds", a.panelSPA)