Insufficient Session Expiration in answerdev/answer
Description
Insufficient Session Expiration in GitHub repository answerdev/answer prior to v1.1.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Insufficient session expiration in Answer before v1.1.0 allows attackers to use old session tokens after a password change.
Root
Cause An insufficient session expiration vulnerability exists in Answer, a Q&A platform, prior to version 1.1.0. The flaw occurs because the application did not invalidate all active sessions for a user when their password was changed, whether through a password reset flow or a direct password update [1][2]. The commit that fixes this issue explicitly adds a call to authService.RemoveUserAllTokens after a password update, ensuring that any pre-existing session tokens become invalid [1].
Exploitation
An attacker who gains access to a valid session token (e.g., through theft, interception, or a user failing to log out on a shared device) can continue to use that token even after the legitimate user changes their password. No additional authentication is required once the token is obtained, and the attacker can access the application using the old session until it naturally expires or is manually revoked [1][2]. The attack does not require any special network position if the token is already in the attacker's possession.
Impact
A successful exploit allows an attacker to maintain unauthorized access to the victim's account indefinitely, despite the user's password change. This compromises the confidentiality and integrity of user data, including account information, private messages, and any other resources accessible through the application. The vulnerability undermines the security purpose of password changes, as the old session tokens are not invalidated [1][2][4].
Mitigation
The vulnerability has been patched in Answer version 1.1.0. Users are strongly advised to upgrade to this or any later version to ensure that all sessions are properly invalidated upon password changes [1][2]. No workaround is available for unpatched versions. The project is hosted on GitHub and maintained under the Apache Answer initiative [3].
- feat(password): logout other user when update password · apache/answer@4f468b5
- NVD - CVE-2023-4126
- GitHub - apache/answer: A Q&A platform software for teams at any scales. Whether it's a community forum, help center, or knowledge management platform, you can always count on Apache Answer.
- The world’s first bug bounty platform for AI/ML
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/answerdev/answerGo | < 1.1.0 | 1.1.0 |
Affected products
2- answerdev/answerdev/answerv5Range: unspecified
Patches
14f468b58d0defeat(password): logout other user when update password
6 files changed · +61 −40
internal/controller/user_controller.go+5 −4 modified@@ -183,9 +183,9 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) { return } - resp, err := uc.userService.UseRePassword(ctx, req) + err := uc.userService.UpdatePasswordWhenForgot(ctx, req) uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) - handler.HandleResponse(ctx, err, resp) + handler.HandleResponse(ctx, err, nil) } // UserLogout user logout @@ -334,15 +334,16 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest" +// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/password [put] func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { - req := &schema.UserModifyPassWordRequest{} + req := &schema.UserModifyPasswordReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.AccessToken = middleware.ExtractToken(ctx) oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req) if err != nil {
internal/repo/auth/auth.go+4 −1 modified@@ -148,7 +148,7 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken } // RemoveUserTokens Log out all users under this user id -func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) { +func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) { key := constant.UserTokenMappingCacheKey + userID resp, _ := ar.data.Cache.GetString(ctx, key) mapping := make(map[string]bool, 0) @@ -158,6 +158,9 @@ func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) { } for token := range mapping { + if token == remainToken { + continue + } if err := ar.RemoveUserCacheInfo(ctx, token); err != nil { log.Error(err) } else {
internal/schema/user_schema.go+11 −7 modified@@ -72,6 +72,8 @@ type GetUserResp struct { RoleID int `json:"role_id"` // user status Status string `json:"status"` + // user have password + HavePassword bool `json:"have_password"` } func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { @@ -83,11 +85,13 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { if ok { r.Status = statusShow } + r.HavePassword = len(userInfo.Pass) > 0 } type GetUserToSetShowResp struct { *GetUserResp - Avatar *AvatarInfo `json:"avatar"` + Avatar *AvatarInfo `json:"avatar"` + HavePassword bool `json:"have_password"` } func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) { @@ -260,14 +264,14 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er return nil, nil } -// UserModifyPassWordRequest -type UserModifyPassWordRequest struct { - UserID string `json:"-" ` // user_id - OldPass string `json:"old_pass" ` // old password - Pass string `json:"pass" ` // password +type UserModifyPasswordReq struct { + OldPass string `json:"old_pass"` + Pass string `json:"pass"` + UserID string `json:"-"` + AccessToken string `json:"-"` } -func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) { +func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) { // TODO i18n err = checker.CheckPassword(8, 32, 0, u.Pass) if err != nil {
internal/service/auth/auth.go+9 −4 modified@@ -20,7 +20,7 @@ type AuthRepo interface { SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) - RemoveUserTokens(ctx context.Context, userID string) + RemoveUserTokens(ctx context.Context, userID string, remainToken string) } // AuthService kit service @@ -85,9 +85,14 @@ func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessTo return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken) } -// RemoveUserTokens Log out all users under this user id -func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) { - as.authRepo.RemoveUserTokens(ctx, userID) +// RemoveUserAllTokens Log out all users under this user id +func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) { + as.authRepo.RemoveUserTokens(ctx, userID, "") +} + +// RemoveTokensExceptCurrentUser remove all tokens except the current user +func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) { + as.authRepo.RemoveUserTokens(ctx, userID, accessToken) } //Admin
internal/service/user_admin/user_backyard.go+2 −2 modified@@ -116,7 +116,7 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda return err } - us.authService.RemoveUserTokens(ctx, req.UserID) + us.authService.RemoveUserAllTokens(ctx, req.UserID) return } @@ -179,7 +179,7 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema. return err } // logout this user - us.authService.RemoveUserTokens(ctx, req.UserID) + us.authService.RemoveUserAllTokens(ctx, req.UserID) return }
internal/service/user_service.go+30 −22 modified@@ -82,6 +82,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st resp.GetFromUserEntity(userInfo) resp.AccessToken = token resp.RoleID = roleID + resp.HavePassword = len(userInfo.Pass) > 0 return resp, nil } @@ -171,42 +172,43 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet return nil } -// UseRePassword -func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) { +// UpdatePasswordWhenForgot update user password when user forgot password +func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil { - return nil, errors.BadRequest(reason.EmailVerifyURLExpired) + return errors.BadRequest(reason.EmailVerifyURLExpired) } userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email) if err != nil { - return nil, err + return err } if !exist { - return nil, errors.BadRequest(reason.UserNotFound) + return errors.BadRequest(reason.UserNotFound) } enpass, err := us.encryptPassword(ctx, req.Pass) if err != nil { - return nil, err + return err } err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) if err != nil { - return nil, err + return err } - resp = &schema.GetUserResp{} - return resp, nil + // When the user changes the password, all the current user's tokens are invalid. + us.authService.RemoveUserAllTokens(ctx, userInfo.ID) + return nil } -func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) { - userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID) +func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) { + userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return false, err } if !has { - return false, fmt.Errorf("user does not exist") + return false, errors.BadRequest(reason.UserNotFound) } - isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass) + isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass) if !isPass { return false, nil } @@ -215,26 +217,29 @@ func (us *UserService) UserModifyPassWordVerification(ctx context.Context, reque } // UserModifyPassword user modify password -func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error { - enpass, err := us.encryptPassword(ctx, request.Pass) +func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error { + enpass, err := us.encryptPassword(ctx, req.Pass) if err != nil { return err } - userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID) + userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return err } - if !has { - return fmt.Errorf("user does not exist") + if !exist { + return errors.BadRequest(reason.UserNotFound) } - isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass) + + isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass) if !isPass { - return fmt.Errorf("the old password verification failed") + return errors.BadRequest(reason.OldPasswordVerificationFailed) } err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) if err != nil { return err } + + us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken) return nil } @@ -477,8 +482,11 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri // verifyPassword // Compare whether the password is correct -func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool { - err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass)) +func (us *UserService) verifyPassword(ctx context.Context, loginPass, userPass string) bool { + if len(loginPass) == 0 && len(userPass) == 0 { + return true + } + err := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass)) return err == nil }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.