diff --git a/docs/Tasktracking/2026-04-25-fix-login-ratelimit-and-ip-spoofing.md b/docs/Tasktracking/2026-04-25-fix-login-ratelimit-and-ip-spoofing.md new file mode 100644 index 00000000..7c8288ee --- /dev/null +++ b/docs/Tasktracking/2026-04-25-fix-login-ratelimit-and-ip-spoofing.md @@ -0,0 +1,18 @@ +# 2026-04-25 Security: Fix login rate limiting and IP spoofing + +## Changes +- Add `RateLimitMiddleware(10, time.Minute)` to `POST /login` endpoint (was unprotected, only register had rate limiting) +- Fix `getRemoteIp()` to use `c.Request.RemoteAddr` instead of trusting `X-Real-IP` / `X-Forwarded-For` headers +- Fix `RateLimitMiddleware` to use `RemoteAddr` directly, preventing IP-based rate limit bypass via header spoofing + +## Security Issue +- Login endpoint had zero rate limiting, enabling unlimited brute-force attempts +- Both IP extraction and rate limiter trusted client-supplied headers, allowing attackers to spoof IPs and bypass all rate limiting + +## Files Modified +- `web/controller/index.go` — add rate limit middleware to login route +- `web/controller/util.go` — use RemoteAddr in getRemoteIp() +- `web/middleware/ratelimit.go` — use RemoteAddr in rate limiter + +## Note +If the panel runs behind a reverse proxy, `RemoteAddr` will show the proxy IP. To restore header-based IP detection, configure `engine.SetTrustedProxies()` in `web/web.go` with the proxy's IP. diff --git a/web/controller/index.go b/web/controller/index.go index 1ada0f57..d66fe940 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -53,7 +53,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) { g.GET("/", a.index) g.GET("/logout", a.logout) - g.POST("/login", a.login) + g.POST("/login", middleware.RateLimitMiddleware(10, time.Minute), a.login) g.POST("/register", middleware.RateLimitMiddleware(5, time.Minute), a.register) g.POST("/getTwoFactorEnable", a.getTwoFactorEnable) g.POST("/getTurnstileSiteKey", a.getTurnstileSiteKey) diff --git a/web/controller/util.go b/web/controller/util.go index df4d4b9f..f1330b9d 100644 --- a/web/controller/util.go +++ b/web/controller/util.go @@ -3,7 +3,6 @@ package controller import ( "net" "net/http" - "strings" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/logger" @@ -13,17 +12,11 @@ import ( "github.com/gin-gonic/gin" ) -// getRemoteIp extracts the real IP address from the request headers or remote address. +// getRemoteIp extracts the real IP address from the direct connection. +// Uses RemoteAddr to prevent IP spoofing via X-Real-IP/X-Forwarded-For headers. +// If the panel is behind a trusted reverse proxy, configure Gin's SetTrustedProxies +// to re-enable header-based IP detection. func getRemoteIp(c *gin.Context) string { - value := c.GetHeader("X-Real-IP") - if value != "" { - return value - } - value = c.GetHeader("X-Forwarded-For") - if value != "" { - ips := strings.Split(value, ",") - return ips[0] - } addr := c.Request.RemoteAddr ip, _, _ := net.SplitHostPort(addr) return ip diff --git a/web/middleware/ratelimit.go b/web/middleware/ratelimit.go index 99396766..a6a26973 100644 --- a/web/middleware/ratelimit.go +++ b/web/middleware/ratelimit.go @@ -37,24 +37,8 @@ func RateLimitMiddleware(maxRequests int, window time.Duration) gin.HandlerFunc }() return func(c *gin.Context) { - ip := c.GetHeader("X-Real-IP") - if ip == "" { - ip = c.GetHeader("X-Forwarded-For") - if ip != "" { - // Take the first IP from X-Forwarded-For - if idx := len(ip); idx > 0 { - for i, ch := range ip { - if ch == ',' { - ip = ip[:i] - break - } - } - } - } - } - if ip == "" { - ip = c.Request.RemoteAddr - } + // Use RemoteAddr directly to prevent IP spoofing via headers + ip := c.Request.RemoteAddr mu.Lock() now := time.Now()