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

Argo CD vulnerable to Bypassing of Brute Force Protection via Application Crash and In-Memory Data Loss

CVE-2024-21652

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 exploit a chain of vulnerabilities, including a Denial of Service (DoS) flaw and in-memory data storage weakness, to effectively bypass the application's brute force login protection. This is a critical security vulnerability that allows attackers to bypass the brute force login protection mechanism. Not only can they crash the service affecting all users, but they can also make unlimited login attempts, increasing the risk of account compromise. Versions 2.8.13, 2.9.9, and 2.10.4 contain a patch for this issue.

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
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)
    +		}
    +	})
    +}
    
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)
    +		}
    +	})
    +}
    

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.