Skip to content

Commit cebb653

Browse files
Merge pull request from GHSA-2vgg-9h6w-m454
* feat: pick random user and exclude admin user and current user from deletion candidates Signed-off-by: pashakostohrys <[email protected]> * feat: increase default max cache size Signed-off-by: pashakostohrys <[email protected]> * add nil protection Signed-off-by: pashakostohrys <[email protected]> * Update util/session/sessionmanager.go Signed-off-by: Dan Garfield <[email protected]> Signed-off-by: Dan Garfield <[email protected]> * chore: fix linter issues Signed-off-by: pashakostohrys <[email protected]> --------- Signed-off-by: pashakostohrys <[email protected]> Signed-off-by: Dan Garfield <[email protected]> Co-authored-by: Dan Garfield <[email protected]>
1 parent ab7e45d commit cebb653

File tree

2 files changed

+60
-15
lines changed

2 files changed

+60
-15
lines changed

util/session/sessionmanager.go

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const (
6969
// Maximum length of username, too keep the cache's memory signature low
7070
maxUsernameLength = 32
7171
// The default maximum session cache size
72-
defaultMaxCacheSize = 1000
72+
defaultMaxCacheSize = 10000
7373
// The default number of maximum login failures before delay kicks in
7474
defaultMaxLoginFailures = 5
7575
// The default time in seconds for the failure window
@@ -310,6 +310,22 @@ func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAtt
310310
return expiredCount
311311
}
312312

313+
// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache.
314+
func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
315+
idx := rand.Intn(len(failures) - 1)
316+
i := 0
317+
for key := range failures {
318+
if i == idx {
319+
if key == common.ArgoCDAdminUsername || key == username {
320+
return pickRandomNonAdminLoginFailure(failures, username)
321+
}
322+
return &key
323+
}
324+
i++
325+
}
326+
return nil
327+
}
328+
313329
// Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
314330
func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
315331

@@ -327,23 +343,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
327343
// prevent overbloating the cache with fake entries, as this could lead to
328344
// memory exhaustion and ultimately in a DoS. We remove a single entry to
329345
// replace it with the new one.
330-
//
331-
// Chances are that we remove the one that is under active attack, but this
332-
// chance is low (1:cache_size)
333346
if failed && len(failures) >= getMaximumCacheSize() {
334347
log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
335-
idx := rand.Intn(len(failures) - 1)
336-
var rmUser string
337-
i := 0
338-
for key := range failures {
339-
if i == idx {
340-
rmUser = key
341-
delete(failures, key)
342-
break
343-
}
344-
i++
348+
rmUser := pickRandomNonAdminLoginFailure(failures, username)
349+
if rmUser != nil {
350+
delete(failures, *rmUser)
351+
log.Infof("Deleted entry for user %s from cache", *rmUser)
345352
}
346-
log.Infof("Deleted entry for user %s from cache", rmUser)
347353
}
348354

349355
attempt, ok := failures[username]

util/session/sessionmanager_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,3 +1173,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL),
11731173
assert.ErrorIs(t, err, common.TokenVerificationErr)
11741174
})
11751175
}
1176+
1177+
func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
1178+
t.Run("Not pick admin user from the queue", func(t *testing.T) {
1179+
failures := map[string]LoginAttempts{
1180+
"admin": {
1181+
FailCount: 1,
1182+
},
1183+
"test2": {
1184+
FailCount: 1,
1185+
},
1186+
}
1187+
1188+
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
1189+
for i := 0; i < 1000; i++ {
1190+
user := pickRandomNonAdminLoginFailure(failures, "test")
1191+
assert.Equal(t, "test2", *user)
1192+
}
1193+
})
1194+
1195+
t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
1196+
failures := map[string]LoginAttempts{
1197+
"test": {
1198+
FailCount: 1,
1199+
},
1200+
"admin": {
1201+
FailCount: 1,
1202+
},
1203+
"test2": {
1204+
FailCount: 1,
1205+
},
1206+
}
1207+
1208+
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
1209+
for i := 0; i < 1000; i++ {
1210+
user := pickRandomNonAdminLoginFailure(failures, "test")
1211+
assert.Equal(t, "test2", *user)
1212+
}
1213+
})
1214+
}

0 commit comments

Comments
 (0)