Argo CD Denial of Service (DoS) Vulnerability Due to Unsafe Array Modification in Multi-threaded Environment
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cdGo | <= 1.8.7 | — |
github.com/argoproj/argo-cd/v2Go | < 2.8.13 | 2.8.13 |
github.com/argoproj/argo-cd/v2Go | >= 2.9.0, < 2.9.9 | 2.9.9 |
github.com/argoproj/argo-cd/v2Go | >= 2.10.0, < 2.10.4 | 2.10.4 |
Affected products
1Patches
32a22e19e06aaMerge pull request from GHSA-6v85-wr92-q4p7
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 {
ce04dc5c6f6eMerge pull request from GHSA-6v85-wr92-q4p7
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 {
5bbb51ab423fMerge pull request from GHSA-6v85-wr92-q4p7
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- github.com/advisories/GHSA-6v85-wr92-q4p7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-21661ghsaADVISORY
- github.com/argoproj/argo-cd/blob/54601c8fd30b86a4c4b7eb449956264372c8bde0/util/session/sessionmanager.goghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/2a22e19e06aaf6a1e734443043310a66c234e345ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/5bbb51ab423f273dda74ab956469843d2db2e208ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/ce04dc5c6f6e92033221ec6d96b74403b065ca8bghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-6v85-wr92-q4p7ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.