VYPR
Moderate severityNVD Advisory· Published Aug 3, 2023· Updated Oct 10, 2024

Insufficient Session Expiration in answerdev/answer

CVE-2023-4126

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].

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.

PackageAffected versionsPatched versions
github.com/answerdev/answerGo
< 1.1.01.1.0

Affected products

2

Patches

1
4f468b58d0de

feat(password): logout other user when update password

https://github.com/answerdev/answerLinkinStarsMay 8, 2023via ghsa
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

4

News mentions

0

No linked articles in our index yet.