mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 22:24:15 +00:00
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:
parent
61f7956af4
commit
77d276da04
4 changed files with 25 additions and 30 deletions
|
|
@ -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.
|
||||||
|
|
@ -53,7 +53,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.GET("/logout", a.logout)
|
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("/register", middleware.RateLimitMiddleware(5, time.Minute), a.register)
|
||||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||||
g.POST("/getTurnstileSiteKey", a.getTurnstileSiteKey)
|
g.POST("/getTurnstileSiteKey", a.getTurnstileSiteKey)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package controller
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
|
@ -13,17 +12,11 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"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 {
|
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
|
addr := c.Request.RemoteAddr
|
||||||
ip, _, _ := net.SplitHostPort(addr)
|
ip, _, _ := net.SplitHostPort(addr)
|
||||||
return ip
|
return ip
|
||||||
|
|
|
||||||
|
|
@ -37,24 +37,8 @@ func RateLimitMiddleware(maxRequests int, window time.Duration) gin.HandlerFunc
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
ip := c.GetHeader("X-Real-IP")
|
// Use RemoteAddr directly to prevent IP spoofing via headers
|
||||||
if ip == "" {
|
ip := c.Request.RemoteAddr
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue