fix: add login rate limiting and prevent IP spoofing via headers

- Add RateLimitMiddleware(10/min) to POST /login (previously unprotected)
- Use RemoteAddr instead of X-Real-IP/X-Forwarded-For in getRemoteIp() and rate limiter
- Prevents brute-force login and rate-limit bypass via spoofed headers
This commit is contained in:
root 2026-04-25 11:43:03 +08:00
parent 61f7956af4
commit 77d276da04
4 changed files with 25 additions and 30 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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()