mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 14:50:59 +00:00
fix(security): bound the login-limiter attempts map
The login rate limiter keys its records on the caller-supplied username and only evicted a record when that exact key was revisited or the login succeeded. An unauthenticated attacker replaying one CSRF token while rotating a fresh username per request seeded a record that was never revisited, growing the map without bound until the panel OOMs. Cap the map: before inserting a new record, reclaim records whose block has lapsed and whose failures aged out, and if the map is still at the ceiling under a broad flood, drop one so memory can never grow past the cap.
This commit is contained in:
@@ -1,10 +1,29 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// An unauthenticated attacker can flood /login with fresh usernames, each of
|
||||
// which seeds a record keyed on that username. The attempts map must stay
|
||||
// bounded rather than growing until the process OOMs.
|
||||
func TestLoginLimiterBoundsMemoryUnderUsernameFlood(t *testing.T) {
|
||||
limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
|
||||
for i := 0; i < loginLimitMaxRecords+100; i++ {
|
||||
limiter.registerFailure("1.2.3.4", "user-"+strconv.Itoa(i))
|
||||
}
|
||||
|
||||
limiter.mu.Lock()
|
||||
n := len(limiter.attempts)
|
||||
limiter.mu.Unlock()
|
||||
|
||||
if n > loginLimitMaxRecords {
|
||||
t.Fatalf("attempts map grew to %d, exceeding the %d ceiling under a username flood", n, loginLimitMaxRecords)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginLimiterBlocksAfterConfiguredFailures(t *testing.T) {
|
||||
now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
|
||||
limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
|
||||
|
||||
Reference in New Issue
Block a user