VYPR
Moderate severityNVD Advisory· Published Mar 18, 2024· Updated Aug 1, 2024

Argo CD vulnerable to Bypassing of Rate Limit and Brute Force Protection Using Cache Overflow

CVE-2024-21662

Description

Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. Prior to versions 2.8.13, 2.9.9, and 2.10.4, an attacker can effectively bypass the rate limit and brute force protections by exploiting the application's weak cache-based mechanism. This loophole in security can be combined with other vulnerabilities to attack the default admin account. This flaw undermines a patch for CVE-2020-8827 intended to protect against brute-force attacks. The application's brute force protection relies on a cache mechanism that tracks login attempts for each user. This cache is limited to a defaultMaxCacheSize of 1000 entries. An attacker can overflow this cache by bombarding it with login attempts for different users, thereby pushing out the admin account's failed attempts and effectively resetting the rate limit for that account. This is a severe vulnerability that enables attackers to perform brute force attacks at an accelerated rate, especially targeting the default admin account. Users should upgrade to version 2.8.13, 2.9.9, or 2.10.4 to receive a patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/argoproj/argo-cd/v2Go
< 2.8.132.8.13
github.com/argoproj/argo-cd/v2Go
>= 2.9.0, < 2.9.92.9.9
github.com/argoproj/argo-cd/v2Go
>= 2.10.0, < 2.10.42.10.4

Affected products

1

Patches

3
cebb6538f794

Merge pull request from GHSA-2vgg-9h6w-m454

https://github.com/argoproj/argo-cdpasha-codefreshMar 18, 2024via ghsa
2 files changed · +60 15
  • util/session/sessionmanager.go+21 15 modified
    @@ -69,7 +69,7 @@ const (
     	// Maximum length of username, too keep the cache's memory signature low
     	maxUsernameLength = 32
     	// The default maximum session cache size
    -	defaultMaxCacheSize = 1000
    +	defaultMaxCacheSize = 10000
     	// The default number of maximum login failures before delay kicks in
     	defaultMaxLoginFailures = 5
     	// The default time in seconds for the failure window
    @@ -310,6 +310,22 @@ func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAtt
     	return expiredCount
     }
     
    +// 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. 
    +func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
    +	idx := rand.Intn(len(failures) - 1)
    +	i := 0
    +	for key := range failures {
    +		if i == idx {
    +			if key == common.ArgoCDAdminUsername || key == username {
    +				return pickRandomNonAdminLoginFailure(failures, username)
    +			}
    +			return &key
    +		}
    +		i++
    +	}
    +	return nil
    +}
    +
     // Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
     func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     
    @@ -327,23 +343,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     	// prevent overbloating the cache with fake entries, as this could lead to
     	// memory exhaustion and ultimately in a DoS. We remove a single entry to
     	// replace it with the new one.
    -	//
    -	// Chances are that we remove the one that is under active attack, but this
    -	// chance is low (1:cache_size)
     	if failed && len(failures) >= getMaximumCacheSize() {
     		log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
    -		idx := rand.Intn(len(failures) - 1)
    -		var rmUser string
    -		i := 0
    -		for key := range failures {
    -			if i == idx {
    -				rmUser = key
    -				delete(failures, key)
    -				break
    -			}
    -			i++
    +		rmUser := pickRandomNonAdminLoginFailure(failures, username)
    +		if rmUser != nil {
    +			delete(failures, *rmUser)
    +			log.Infof("Deleted entry for user %s from cache", *rmUser)
     		}
    -		log.Infof("Deleted entry for user %s from cache", rmUser)
     	}
     
     	attempt, ok := failures[username]
    
  • util/session/sessionmanager_test.go+39 0 modified
    @@ -1173,3 +1173,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL),
     		assert.ErrorIs(t, err, common.TokenVerificationErr)
     	})
     }
    +
    +func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
    +	t.Run("Not pick admin user from the queue", func(t *testing.T) {
    +		failures := map[string]LoginAttempts{
    +			"admin": {
    +				FailCount: 1,
    +			},
    +			"test2": {
    +				FailCount: 1,
    +			},
    +		}
    +
    +		// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
    +		for i := 0; i < 1000; i++ {
    +			user := pickRandomNonAdminLoginFailure(failures, "test")
    +			assert.Equal(t, "test2", *user)
    +		}
    +	})
    +
    +	t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
    +		failures := map[string]LoginAttempts{
    +			"test": {
    +				FailCount: 1,
    +			},
    +			"admin": {
    +				FailCount: 1,
    +			},
    +			"test2": {
    +				FailCount: 1,
    +			},
    +		}
    +
    +		// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
    +		for i := 0; i < 1000; i++ {
    +			user := pickRandomNonAdminLoginFailure(failures, "test")
    +			assert.Equal(t, "test2", *user)
    +		}
    +	})
    +}
    
17b0df1168a4

Merge pull request from GHSA-2vgg-9h6w-m454

https://github.com/argoproj/argo-cdpasha-codefreshMar 18, 2024via ghsa
2 files changed · +60 15
  • util/session/sessionmanager.go+21 15 modified
    @@ -69,7 +69,7 @@ const (
     	// Maximum length of username, too keep the cache's memory signature low
     	maxUsernameLength = 32
     	// The default maximum session cache size
    -	defaultMaxCacheSize = 1000
    +	defaultMaxCacheSize = 10000
     	// The default number of maximum login failures before delay kicks in
     	defaultMaxLoginFailures = 5
     	// The default time in seconds for the failure window
    @@ -310,6 +310,22 @@ func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAtt
     	return expiredCount
     }
     
    +// 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. 
    +func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
    +	idx := rand.Intn(len(failures) - 1)
    +	i := 0
    +	for key := range failures {
    +		if i == idx {
    +			if key == common.ArgoCDAdminUsername || key == username {
    +				return pickRandomNonAdminLoginFailure(failures, username)
    +			}
    +			return &key
    +		}
    +		i++
    +	}
    +	return nil
    +}
    +
     // Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
     func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     
    @@ -327,23 +343,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     	// prevent overbloating the cache with fake entries, as this could lead to
     	// memory exhaustion and ultimately in a DoS. We remove a single entry to
     	// replace it with the new one.
    -	//
    -	// Chances are that we remove the one that is under active attack, but this
    -	// chance is low (1:cache_size)
     	if failed && len(failures) >= getMaximumCacheSize() {
     		log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
    -		idx := rand.Intn(len(failures) - 1)
    -		var rmUser string
    -		i := 0
    -		for key := range failures {
    -			if i == idx {
    -				rmUser = key
    -				delete(failures, key)
    -				break
    -			}
    -			i++
    +		rmUser := pickRandomNonAdminLoginFailure(failures, username)
    +		if rmUser != nil {
    +			delete(failures, *rmUser)
    +			log.Infof("Deleted entry for user %s from cache", *rmUser)
     		}
    -		log.Infof("Deleted entry for user %s from cache", rmUser)
     	}
     
     	attempt, ok := failures[username]
    
  • util/session/sessionmanager_test.go+39 0 modified
    @@ -1188,3 +1188,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL),
     		assert.ErrorIs(t, err, common.TokenVerificationErr)
     	})
     }
    +
    +func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
    +	t.Run("Not pick admin user from the queue", func(t *testing.T) {
    +		failures := map[string]LoginAttempts{
    +			"admin": {
    +				FailCount: 1,
    +			},
    +			"test2": {
    +				FailCount: 1,
    +			},
    +		}
    +
    +		// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
    +		for i := 0; i < 1000; i++ {
    +			user := pickRandomNonAdminLoginFailure(failures, "test")
    +			assert.Equal(t, "test2", *user)
    +		}
    +	})
    +
    +	t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
    +		failures := map[string]LoginAttempts{
    +			"test": {
    +				FailCount: 1,
    +			},
    +			"admin": {
    +				FailCount: 1,
    +			},
    +			"test2": {
    +				FailCount: 1,
    +			},
    +		}
    +
    +		// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
    +		for i := 0; i < 1000; i++ {
    +			user := pickRandomNonAdminLoginFailure(failures, "test")
    +			assert.Equal(t, "test2", *user)
    +		}
    +	})
    +}
    
6e181d72b315

Merge pull request from GHSA-2vgg-9h6w-m454

https://github.com/argoproj/argo-cdpasha-codefreshMar 18, 2024via ghsa
2 files changed · +60 15
  • util/session/sessionmanager.go+21 15 modified
    @@ -69,7 +69,7 @@ const (
     	// Maximum length of username, too keep the cache's memory signature low
     	maxUsernameLength = 32
     	// The default maximum session cache size
    -	defaultMaxCacheSize = 1000
    +	defaultMaxCacheSize = 10000
     	// The default number of maximum login failures before delay kicks in
     	defaultMaxLoginFailures = 5
     	// The default time in seconds for the failure window
    @@ -310,6 +310,22 @@ func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAtt
     	return expiredCount
     }
     
    +// 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. 
    +func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
    +	idx := rand.Intn(len(failures) - 1)
    +	i := 0
    +	for key := range failures {
    +		if i == idx {
    +			if key == common.ArgoCDAdminUsername || key == username {
    +				return pickRandomNonAdminLoginFailure(failures, username)
    +			}
    +			return &key
    +		}
    +		i++
    +	}
    +	return nil
    +}
    +
     // Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
     func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     
    @@ -327,23 +343,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     	// prevent overbloating the cache with fake entries, as this could lead to
     	// memory exhaustion and ultimately in a DoS. We remove a single entry to
     	// replace it with the new one.
    -	//
    -	// Chances are that we remove the one that is under active attack, but this
    -	// chance is low (1:cache_size)
     	if failed && len(failures) >= getMaximumCacheSize() {
     		log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
    -		idx := rand.Intn(len(failures) - 1)
    -		var rmUser string
    -		i := 0
    -		for key := range failures {
    -			if i == idx {
    -				rmUser = key
    -				delete(failures, key)
    -				break
    -			}
    -			i++
    +		rmUser := pickRandomNonAdminLoginFailure(failures, username)
    +		if rmUser != nil {
    +			delete(failures, *rmUser)
    +			log.Infof("Deleted entry for user %s from cache", *rmUser)
     		}
    -		log.Infof("Deleted entry for user %s from cache", rmUser)
     	}
     
     	attempt, ok := failures[username]
    
  • util/session/sessionmanager_test.go+39 0 modified
    @@ -1173,3 +1173,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL),
     		assert.ErrorIs(t, err, common.TokenVerificationErr)
     	})
     }
    +
    +func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
    +	t.Run("Not pick admin user from the queue", func(t *testing.T) {
    +		failures := map[string]LoginAttempts{
    +			"admin": {
    +				FailCount: 1,
    +			},
    +			"test2": {
    +				FailCount: 1,
    +			},
    +		}
    +
    +		// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
    +		for i := 0; i < 1000; i++ {
    +			user := pickRandomNonAdminLoginFailure(failures, "test")
    +			assert.Equal(t, "test2", *user)
    +		}
    +	})
    +
    +	t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
    +		failures := map[string]LoginAttempts{
    +			"test": {
    +				FailCount: 1,
    +			},
    +			"admin": {
    +				FailCount: 1,
    +			},
    +			"test2": {
    +				FailCount: 1,
    +			},
    +		}
    +
    +		// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
    +		for i := 0; i < 1000; i++ {
    +			user := pickRandomNonAdminLoginFailure(failures, "test")
    +			assert.Equal(t, "test2", *user)
    +		}
    +	})
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.