mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +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("/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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue