gorest InMemorySecret2FA race condition allows process crash via concurrent map access (CWE-362)
Description
A race condition in gorest's InMemorySecret2FA map allows unauthenticated remote attackers to crash the process via concurrent requests.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A race condition in gorest's InMemorySecret2FA map allows unauthenticated remote attackers to crash the process via concurrent requests.
Vulnerability
The InMemorySecret2FA in github.com/pilinux/gorest (versions < 1.12.2) uses a bare map[uint64]Secret2FA without synchronization (CWE-362). Multiple HTTP handlers (login, 2FA setup, activation, verification, and deletion) perform concurrent reads and writes on this map without a sync.RWMutex, leading to Go runtime fatal errors. [1][2]
Exploitation
An attacker can send multiple concurrent HTTP requests to any of the affected endpoints (e.g., login, 2FA setup, activation, verification) to trigger simultaneous map reads and writes. No authentication is required; the race condition can be triggered by unauthenticated requests if the endpoints are exposed. The attacker only needs network access to the server. [1]
Impact
Successful exploitation causes the Go runtime to detect concurrent map access and terminate the process with a fatal error ("concurrent map read and map write" or "concurrent map writes"), resulting in a denial of service (DoS). No data confidentiality or integrity is compromised. [1][2]
Mitigation
The fix was introduced in pull request #391 and is included in version 1.12.2, released on May 17, 2026. The fix replaces the bare map with a Secret2FAStore protected by sync.RWMutex. Users should upgrade to v1.12.2 or later. No workaround is available for earlier versions. [3]
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
1117ff55fc21b修复InMemorySecret2FA全局map竞态条件 (#391)
4 files changed · +62 −9
database/model/twoFA.go+54 −1 modified@@ -1,6 +1,7 @@ package model import ( + "sync" "time" "gorm.io/gorm" @@ -38,6 +39,58 @@ type Secret2FA struct { Image string `json:"-"` } +// cloneSecret2FA returns a deep copy of a Secret2FA. +// This prevents external code from mutating the store's data +// through shared slice backing arrays. +func cloneSecret2FA(v Secret2FA) Secret2FA { + out := Secret2FA{Image: v.Image} + if v.PassHash != nil { + out.PassHash = append([]byte(nil), v.PassHash...) + } + if v.KeySalt != nil { + out.KeySalt = append([]byte(nil), v.KeySalt...) + } + if v.Secret != nil { + out.Secret = append([]byte(nil), v.Secret...) + } + return out +} + +// Secret2FAStore provides thread-safe access to in-memory 2FA secrets. +type Secret2FAStore struct { + mu sync.RWMutex + data map[uint64]Secret2FA +} + +// NewSecret2FAStore creates a new Secret2FAStore. +func NewSecret2FAStore() *Secret2FAStore { + return &Secret2FAStore{ + data: make(map[uint64]Secret2FA), + } +} + +// Get retrieves a Secret2FA from the store. +func (s *Secret2FAStore) Get(key uint64) (Secret2FA, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + v, ok := s.data[key] + return cloneSecret2FA(v), ok +} + +// Set stores a Secret2FA in the store. +func (s *Secret2FAStore) Set(key uint64, value Secret2FA) { + s.mu.Lock() + defer s.mu.Unlock() + s.data[key] = cloneSecret2FA(value) +} + +// Delete removes a Secret2FA from the store. +func (s *Secret2FAStore) Delete(key uint64) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.data, key) +} + // InMemorySecret2FA keeps secrets temporarily // in memory to set up 2FA. -var InMemorySecret2FA = make(map[uint64]Secret2FA) +var InMemorySecret2FA = NewSecret2FAStore()
handler/login.go+1 −1 modified@@ -136,7 +136,7 @@ func Login(payload model.AuthPayload) (httpResponse model.HTTPResponse, httpStat // save the hashed pass (key) in memory for OTP validation step data2FA := model.Secret2FA{} data2FA.PassHash = key - model.InMemorySecret2FA[claims.AuthID] = data2FA + model.InMemorySecret2FA.Set(claims.AuthID, data2FA) } } }
handler/twoFA.go+6 −6 modified@@ -186,7 +186,7 @@ func Setup2FA(claims middleware.MyCustomClaims, authPayload model.AuthPayload) ( key := lib.GetArgon2Key([]byte(authPayload.Password), salt, 32) // step 6: check if client secret is available in memory - data2FA, ok := model.InMemorySecret2FA[claims.AuthID] + data2FA, ok := model.InMemorySecret2FA.Get(claims.AuthID) if ok { // delete old QR image if available if lib.FileExist(configSecurity.TwoFA.PathQR + "/" + data2FA.Image) { @@ -202,7 +202,7 @@ func Setup2FA(claims middleware.MyCustomClaims, authPayload model.AuthPayload) ( data2FA.KeySalt = salt data2FA.Secret = otpByte data2FA.Image = img - model.InMemorySecret2FA[claims.AuthID] = data2FA + model.InMemorySecret2FA.Set(claims.AuthID, data2FA) // serve the QR to the client httpResponse.Message = configSecurity.TwoFA.PathQR + "/" + img @@ -242,7 +242,7 @@ func Activate2FA(claims middleware.MyCustomClaims, authPayload model.AuthPayload // start 2FA activation procedure // // step 1: check if client secret is available in memory - data2FA, ok := model.InMemorySecret2FA[claims.AuthID] + data2FA, ok := model.InMemorySecret2FA.Get(claims.AuthID) if !ok { // request user to visit setup endpoint first httpResponse.Message = "request for a new 2-fa secret" @@ -269,7 +269,7 @@ func Activate2FA(claims middleware.MyCustomClaims, authPayload model.AuthPayload if status == configSecurity.TwoFA.Status.Invalid { // save the secret with failed attempt in memory for future validation procedure data2FA.Secret = otpByte - model.InMemorySecret2FA[claims.AuthID] = data2FA + model.InMemorySecret2FA.Set(claims.AuthID, data2FA) httpResponse.Message = "wrong one-time password" httpStatusCode = http.StatusBadRequest @@ -488,7 +488,7 @@ func Validate2FA(claims middleware.MyCustomClaims, authPayload model.AuthPayload // start 2FA validation procedure // // step 1: check if client secret is available in memory - data2FA, ok := model.InMemorySecret2FA[claims.AuthID] + data2FA, ok := model.InMemorySecret2FA.Get(claims.AuthID) if !ok { httpResponse.Message = "log in again" httpStatusCode = http.StatusBadRequest @@ -572,7 +572,7 @@ func Validate2FA(claims middleware.MyCustomClaims, authPayload model.AuthPayload if status == configSecurity.TwoFA.Status.Invalid { // save the new secret in memory for future validation procedure data2FA.Secret = otpByte - model.InMemorySecret2FA[claims.AuthID] = data2FA + model.InMemorySecret2FA.Set(claims.AuthID, data2FA) // save in DB to protect from accidental data loss //
service/common.go+1 −1 modified@@ -76,7 +76,7 @@ func Validate2FA(encryptedMessage []byte, issuer string, userInput string) ([]by // DelMem2FA deletes 2FA secrets from memory. func DelMem2FA(authID uint64) { - delete(model.InMemorySecret2FA, authID) + model.InMemorySecret2FA.Delete(authID) } // SendEmail sends a verification/password recovery email if
Vulnerability mechanics
Root cause
"The `InMemorySecret2FA` variable in `database/model/twoFA.go` is a bare Go map with no mutex or other synchronization primitive, so concurrent reads and writes from multiple HTTP handlers trigger an unrecoverable Go runtime fatal error."
Attack vector
An attacker triggers the bug by causing concurrent HTTP requests that simultaneously read from and write to the bare `InMemorySecret2FA` map. For example, two users with 2FA enabled logging in at the same time produce concurrent map writes at `handler/login.go:139`, or one user logging in while another performs 2FA verification produces a concurrent read+write pair [ref_id=1]. Go's runtime detects the unsynchronized access and throws an unrecoverable `fatal error: concurrent map read and map write` (or `concurrent map writes`), crashing the entire process [ref_id=2]. No authentication is required because the race is triggered at the login endpoint, which is publicly accessible. An attacker can repeatedly issue concurrent requests to crash the service on demand, achieving a denial of service.
What the fix does
The patch wraps the bare `map[uint64]Secret2FA` inside a new `Secret2FAStore` struct that holds a `sync.RWMutex` [ref_id=1]. All read operations call `RLock` (shared read lock) and all write/delete operations call `Lock` (exclusive write lock), ensuring that no two goroutines access the map concurrently without synchronization [patch_id=5725882]. The `Get` method also returns a deep copy via `cloneSecret2FA` to prevent callers from mutating the store's data through shared slice backing arrays after the lock is released. All nine handler call sites were updated from direct map access to the corresponding store method calls, eliminating the race condition.
Reproduction
Simulate two concurrent logins with 2FA enabled: ```bash for i in 1 2; do curl -X POST http://target:8080/api/v1/login \ -H "Content-Type: application/json" \ -d '{"email":"user'$i'@example.com","password":"testpass"}' & done wait ``` The Go runtime prints `fatal error: concurrent map writes` and the process crashes [ref_id=1][ref_id=2].
Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.