VYPR
High severityNVD Advisory· Published Mar 18, 2024· Updated Aug 2, 2024

Argo CD Denial of Service (DoS) Vulnerability Due to Unsafe Array Modification in Multi-threaded Environment

CVE-2024-21661

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 critical flaw in the application to initiate a Denial of Service (DoS) attack, rendering the application inoperable and affecting all users. The issue arises from unsafe manipulation of an array in a multi-threaded environment. The vulnerability is rooted in the application's code, where an array is being modified while it is being iterated over. This is a classic programming error but becomes critically unsafe when executed in a multi-threaded environment. When two threads interact with the same array simultaneously, the application crashes. This is a Denial of Service (DoS) vulnerability. Any attacker can crash the application continuously, making it impossible for legitimate users to access the service. The issue is exacerbated because it does not require authentication, widening the pool of potential attackers. 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-cdGo
<= 1.8.7
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
2a22e19e06aa

Merge pull request from GHSA-6v85-wr92-q4p7

https://github.com/argoproj/argo-cdDan GarfieldMar 18, 2024via ghsa
4 files changed · +16 10
  • server/application/terminal.go+2 2 modified
    @@ -38,12 +38,12 @@ type terminalHandler struct {
     	allowedShells     []string
     	namespace         string
     	enabledNamespaces []string
    -	sessionManager    util_session.SessionManager
    +	sessionManager    *util_session.SessionManager
     }
     
     // NewHandler returns a new terminal handler.
     func NewHandler(appLister applisters.ApplicationLister, namespace string, enabledNamespaces []string, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache,
    -	appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager util_session.SessionManager) *terminalHandler {
    +	appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager *util_session.SessionManager) *terminalHandler {
     	return &terminalHandler{
     		appLister:         appLister,
     		db:                db,
    
  • server/application/websocket.go+2 2 modified
    @@ -37,7 +37,7 @@ type terminalSession struct {
     	tty            bool
     	readLock       sync.Mutex
     	writeLock      sync.Mutex
    -	sessionManager util_session.SessionManager
    +	sessionManager *util_session.SessionManager
     	token          *string
     }
     
    @@ -48,7 +48,7 @@ func getToken(r *http.Request) (string, error) {
     }
     
     // newTerminalSession create terminalSession
    -func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager util_session.SessionManager) (*terminalSession, error) {
    +func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) {
     	token, err := getToken(r)
     	if err != nil {
     		return nil, err
    
  • server/server.go+1 1 modified
    @@ -982,7 +982,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
     	}
     	mux.Handle("/api/", handler)
     
    -	terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr).
    +	terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr).
     		WithFeatureFlagMiddleware(a.settingsMgr.GetSettings)
     	th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal)
     	mux.Handle("/terminal", th)
    
  • util/session/sessionmanager.go+11 5 modified
    @@ -10,6 +10,7 @@ import (
     	"net/http"
     	"os"
     	"strings"
    +	"sync"
     	"time"
     
     	"github.com/coreos/go-oidc/v3/oidc"
    @@ -41,6 +42,7 @@ type SessionManager struct {
     	storage                       UserStateStorage
     	sleep                         func(d time.Duration)
     	verificationDelayNoiseEnabled bool
    +	failedLock                    sync.RWMutex
     }
     
     // LoginAttempts is a timestamped counter for failed login attempts
    @@ -284,7 +286,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
     	return token.Claims, newToken, nil
     }
     
    -// GetLoginFailures retrieves the login failure information from the cache
    +// GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner.
     func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
     	// Get failures from the cache
     	var failures map[string]LoginAttempts
    @@ -299,12 +301,12 @@ func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
     	return failures
     }
     
    -func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int {
    +func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int {
     	expiredCount := 0
    -	for key, attempt := range *failures {
    +	for key, attempt := range failures {
     		if time.Since(attempt.LastFailed) > maxAge*time.Second {
     			expiredCount += 1
    -			delete(*failures, key)
    +			delete(failures, key)
     		}
     	}
     	return expiredCount
    @@ -328,12 +330,14 @@ func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username
     
     // 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) {
    +	mgr.failedLock.Lock()
    +	defer mgr.failedLock.Unlock()
     
     	failures := mgr.GetLoginFailures()
     
     	// Expire old entries in the cache if we have a failure window defined.
     	if window := getLoginFailureWindow(); window > 0 {
    -		count := expireOldFailedAttempts(window, &failures)
    +		count := expireOldFailedAttempts(window, failures)
     		if count > 0 {
     			log.Infof("Expired %d entries from session cache due to max age reached", count)
     		}
    @@ -380,6 +384,8 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     
     // Get the current login failure attempts for given username
     func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
    +	mgr.failedLock.RLock()
    +	defer mgr.failedLock.RUnlock()
     	failures := mgr.GetLoginFailures()
     	attempt, ok := failures[username]
     	if !ok {
    
ce04dc5c6f6e

Merge pull request from GHSA-6v85-wr92-q4p7

https://github.com/argoproj/argo-cdDan GarfieldMar 18, 2024via ghsa
4 files changed · +16 10
  • server/application/terminal.go+2 2 modified
    @@ -38,12 +38,12 @@ type terminalHandler struct {
     	allowedShells     []string
     	namespace         string
     	enabledNamespaces []string
    -	sessionManager    util_session.SessionManager
    +	sessionManager    *util_session.SessionManager
     }
     
     // NewHandler returns a new terminal handler.
     func NewHandler(appLister applisters.ApplicationLister, namespace string, enabledNamespaces []string, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache,
    -	appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager util_session.SessionManager) *terminalHandler {
    +	appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager *util_session.SessionManager) *terminalHandler {
     	return &terminalHandler{
     		appLister:         appLister,
     		db:                db,
    
  • server/application/websocket.go+2 2 modified
    @@ -37,7 +37,7 @@ type terminalSession struct {
     	tty            bool
     	readLock       sync.Mutex
     	writeLock      sync.Mutex
    -	sessionManager util_session.SessionManager
    +	sessionManager *util_session.SessionManager
     	token          *string
     }
     
    @@ -48,7 +48,7 @@ func getToken(r *http.Request) (string, error) {
     }
     
     // newTerminalSession create terminalSession
    -func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager util_session.SessionManager) (*terminalSession, error) {
    +func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) {
     	token, err := getToken(r)
     	if err != nil {
     		return nil, err
    
  • server/server.go+1 1 modified
    @@ -997,7 +997,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
     	}
     	mux.Handle("/api/", handler)
     
    -	terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr).
    +	terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr).
     		WithFeatureFlagMiddleware(a.settingsMgr.GetSettings)
     	th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal)
     	mux.Handle("/terminal", th)
    
  • util/session/sessionmanager.go+11 5 modified
    @@ -10,6 +10,7 @@ import (
     	"net/http"
     	"os"
     	"strings"
    +	"sync"
     	"time"
     
     	"github.com/coreos/go-oidc/v3/oidc"
    @@ -41,6 +42,7 @@ type SessionManager struct {
     	storage                       UserStateStorage
     	sleep                         func(d time.Duration)
     	verificationDelayNoiseEnabled bool
    +	failedLock                    sync.RWMutex
     }
     
     // LoginAttempts is a timestamped counter for failed login attempts
    @@ -284,7 +286,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
     	return token.Claims, newToken, nil
     }
     
    -// GetLoginFailures retrieves the login failure information from the cache
    +// GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner.
     func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
     	// Get failures from the cache
     	var failures map[string]LoginAttempts
    @@ -299,12 +301,12 @@ func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
     	return failures
     }
     
    -func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int {
    +func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int {
     	expiredCount := 0
    -	for key, attempt := range *failures {
    +	for key, attempt := range failures {
     		if time.Since(attempt.LastFailed) > maxAge*time.Second {
     			expiredCount += 1
    -			delete(*failures, key)
    +			delete(failures, key)
     		}
     	}
     	return expiredCount
    @@ -328,12 +330,14 @@ func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username
     
     // 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) {
    +	mgr.failedLock.Lock()
    +	defer mgr.failedLock.Unlock()
     
     	failures := mgr.GetLoginFailures()
     
     	// Expire old entries in the cache if we have a failure window defined.
     	if window := getLoginFailureWindow(); window > 0 {
    -		count := expireOldFailedAttempts(window, &failures)
    +		count := expireOldFailedAttempts(window, failures)
     		if count > 0 {
     			log.Infof("Expired %d entries from session cache due to max age reached", count)
     		}
    @@ -380,6 +384,8 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     
     // Get the current login failure attempts for given username
     func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
    +	mgr.failedLock.RLock()
    +	defer mgr.failedLock.RUnlock()
     	failures := mgr.GetLoginFailures()
     	attempt, ok := failures[username]
     	if !ok {
    
5bbb51ab423f

Merge pull request from GHSA-6v85-wr92-q4p7

https://github.com/argoproj/argo-cdDan GarfieldMar 18, 2024via ghsa
4 files changed · +16 10
  • server/application/terminal.go+2 2 modified
    @@ -38,12 +38,12 @@ type terminalHandler struct {
     	allowedShells     []string
     	namespace         string
     	enabledNamespaces []string
    -	sessionManager    util_session.SessionManager
    +	sessionManager    *util_session.SessionManager
     }
     
     // NewHandler returns a new terminal handler.
     func NewHandler(appLister applisters.ApplicationLister, namespace string, enabledNamespaces []string, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache,
    -	appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager util_session.SessionManager) *terminalHandler {
    +	appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager *util_session.SessionManager) *terminalHandler {
     	return &terminalHandler{
     		appLister:         appLister,
     		db:                db,
    
  • server/application/websocket.go+2 2 modified
    @@ -37,7 +37,7 @@ type terminalSession struct {
     	tty            bool
     	readLock       sync.Mutex
     	writeLock      sync.Mutex
    -	sessionManager util_session.SessionManager
    +	sessionManager *util_session.SessionManager
     	token          *string
     }
     
    @@ -48,7 +48,7 @@ func getToken(r *http.Request) (string, error) {
     }
     
     // newTerminalSession create terminalSession
    -func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager util_session.SessionManager) (*terminalSession, error) {
    +func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) {
     	token, err := getToken(r)
     	if err != nil {
     		return nil, err
    
  • server/server.go+1 1 modified
    @@ -997,7 +997,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
     	}
     	mux.Handle("/api/", handler)
     
    -	terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr).
    +	terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr).
     		WithFeatureFlagMiddleware(a.settingsMgr.GetSettings)
     	th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal)
     	mux.Handle("/terminal", th)
    
  • util/session/sessionmanager.go+11 5 modified
    @@ -10,6 +10,7 @@ import (
     	"net/http"
     	"os"
     	"strings"
    +	"sync"
     	"time"
     
     	"github.com/coreos/go-oidc/v3/oidc"
    @@ -41,6 +42,7 @@ type SessionManager struct {
     	storage                       UserStateStorage
     	sleep                         func(d time.Duration)
     	verificationDelayNoiseEnabled bool
    +	failedLock                    sync.RWMutex
     }
     
     // LoginAttempts is a timestamped counter for failed login attempts
    @@ -284,7 +286,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
     	return token.Claims, newToken, nil
     }
     
    -// GetLoginFailures retrieves the login failure information from the cache
    +// GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner.
     func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
     	// Get failures from the cache
     	var failures map[string]LoginAttempts
    @@ -299,12 +301,12 @@ func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
     	return failures
     }
     
    -func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int {
    +func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int {
     	expiredCount := 0
    -	for key, attempt := range *failures {
    +	for key, attempt := range failures {
     		if time.Since(attempt.LastFailed) > maxAge*time.Second {
     			expiredCount += 1
    -			delete(*failures, key)
    +			delete(failures, key)
     		}
     	}
     	return expiredCount
    @@ -328,12 +330,14 @@ func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username
     
     // 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) {
    +	mgr.failedLock.Lock()
    +	defer mgr.failedLock.Unlock()
     
     	failures := mgr.GetLoginFailures()
     
     	// Expire old entries in the cache if we have a failure window defined.
     	if window := getLoginFailureWindow(); window > 0 {
    -		count := expireOldFailedAttempts(window, &failures)
    +		count := expireOldFailedAttempts(window, failures)
     		if count > 0 {
     			log.Infof("Expired %d entries from session cache due to max age reached", count)
     		}
    @@ -380,6 +384,8 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
     
     // Get the current login failure attempts for given username
     func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
    +	mgr.failedLock.RLock()
    +	defer mgr.failedLock.RUnlock()
     	failures := mgr.GetLoginFailures()
     	attempt, ok := failures[username]
     	if !ok {
    

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.