CVE-2023-4126: Insufficient Session Expiration (CWE-613)
value: Insufficient Session Expiration in GitHub repository answerdev/answer prior to v1.1.0.
CWE description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
CVSS v3 base score: 8.8
Published: 2023-08-03T04:15Z
Repository: https://github.com/answerdev/answer
Language: Go
Files changed: internal/controller/user_controller.go, internal/repo/auth/auth.go, internal/schema/user_schema.go, internal/service/auth/auth.go, internal/service/user_admin/user_backyard.go, internal/service/user_service.go
Commit date: 2023-05-08 17:51:29 +0800
Commit message: feat(password): logout other user when update password

Diff: diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go
index 4fb7f8a6d..56e0ef4e9 100644
--- a/internal/controller/user_controller.go
+++ b/internal/controller/user_controller.go
@@ -181,13 +181,13 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
 		handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
 			&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
 		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
 // @Summary user logout
 // @Description user logout
@@ -332,19 +332,20 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
 // @Description UserModifyPassWord
 // @Tags User
 // @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 {
 		handler.HandleResponse(ctx, err, nil)
 		return
diff --git a/internal/repo/auth/auth.go b/internal/repo/auth/auth.go
index bcc512126..5cd808a72 100644
--- a/internal/repo/auth/auth.go
+++ b/internal/repo/auth/auth.go
@@ -146,20 +146,23 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
 	content, _ := json.Marshal(mapping)
 	return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime)
 }
 
 // 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)
 	if len(resp) > 0 {
 		_ = json.Unmarshal([]byte(resp), &mapping)
 		log.Debugf("find %d user tokens by user id %s", len(mapping), userID)
 	}
 
 	for token := range mapping {
+		if token == remainToken {
+			continue
+		}
 		if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
 			log.Error(err)
 		} else {
 			log.Debugf("del user %s token success")
 		}
diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go
index 579a55ddd..9097b0ffb 100644
--- a/internal/schema/user_schema.go
+++ b/internal/schema/user_schema.go
@@ -70,10 +70,12 @@ type GetUserResp struct {
 	AccessToken string `json:"access_token"`
 	// role id
 	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) {
 	_ = copier.Copy(r, userInfo)
 	r.Avatar = FormatAvatarInfo(userInfo.Avatar, userInfo.EMail)
@@ -81,15 +83,17 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
 	r.LastLoginDate = userInfo.LastLoginDate.Unix()
 	statusShow, ok := UserStatusShow[userInfo.Status]
 	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) {
 	_ = copier.Copy(r, userInfo)
 	r.CreatedAt = userInfo.CreatedAt.Unix()
@@ -258,18 +262,18 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
 		return errFields, err
 	}
 	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 {
 		errField := &validator.FormErrorField{
 			ErrorField: "pass",
diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go
index 618ef5bdf..139c58ae8 100644
--- a/internal/service/auth/auth.go
+++ b/internal/service/auth/auth.go
@@ -18,11 +18,11 @@ type AuthRepo interface {
 	RemoveUserStatus(ctx context.Context, userID string) (err error)
 	GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error)
 	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
 type AuthService struct {
 	authRepo AuthRepo
@@ -83,13 +83,18 @@ func (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken stri
 // AddUserTokenMapping add user token mapping
 func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {
 	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
 
 func (as *AuthService) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go
index 6897d8d28..a98086aef 100644
--- a/internal/service/user_admin/user_backyard.go
+++ b/internal/service/user_admin/user_backyard.go
@@ -114,11 +114,11 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda
 	err = us.userRoleRelService.SaveUserRole(ctx, req.UserID, req.RoleID)
 	if err != nil {
 		return err
 	}
 
-	us.authService.RemoveUserTokens(ctx, req.UserID)
+	us.authService.RemoveUserAllTokens(ctx, req.UserID)
 	return
 }
 
 // AddUser add user
 func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) {
@@ -177,11 +177,11 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.
 	err = us.userRepo.UpdateUserPassword(ctx, userInfo.ID, string(hashPwd))
 	if err != nil {
 		return err
 	}
 	// logout this user
-	us.authService.RemoveUserTokens(ctx, req.UserID)
+	us.authService.RemoveUserAllTokens(ctx, req.UserID)
 	return
 }
 
 // GetUserInfo get user one
 func (us *UserAdminService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) {
diff --git a/internal/service/user_service.go b/internal/service/user_service.go
index 850a260ac..a857ba59f 100644
--- a/internal/service/user_service.go
+++ b/internal/service/user_service.go
@@ -80,10 +80,11 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
 	}
 	resp = &schema.GetUserToSetShowResp{}
 	resp.GetFromUserEntity(userInfo)
 	resp.AccessToken = token
 	resp.RoleID = roleID
+	resp.HavePassword = len(userInfo.Pass) > 0
 	return resp, nil
 }
 
 func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) (
 	resp *schema.GetOtherUserInfoByUsernameResp, err error,
@@ -169,74 +170,78 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
 	}
 	go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
 	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
 	}
 
 	return true, nil
 }
 
 // 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
 }
 
 // UpdateInfo update user info
 func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (
@@ -475,12 +480,15 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
 	return resp, nil
 }
 
 // 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
 }
 
 // encryptPassword
 // The password does irreversible encryption.

Vulnerable code: 	resp, err := uc.userService.UseRePassword(ctx, req)
	handler.HandleResponse(ctx, err, resp)
// @Param data body schema.UserModifyPassWordRequest  true "UserModifyPassWordRequest"
	req := &schema.UserModifyPassWordRequest{}
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
	Avatar *AvatarInfo `json:"avatar"`
// UserModifyPassWordRequest
type UserModifyPassWordRequest struct {
	UserID  string `json:"-" `        // user_id
	OldPass string `json:"old_pass" ` // old password
	Pass    string `json:"pass" `     // password
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
	RemoveUserTokens(ctx context.Context, userID string)
// RemoveUserTokens Log out all users under this user id
func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) {
	as.authRepo.RemoveUserTokens(ctx, userID)
	us.authService.RemoveUserTokens(ctx, req.UserID)
	us.authService.RemoveUserTokens(ctx, req.UserID)
// UseRePassword
func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) {
		return nil, errors.BadRequest(reason.EmailVerifyURLExpired)
		return nil, err
		return nil, errors.BadRequest(reason.UserNotFound)
		return nil, err
		return nil, err
	resp = &schema.GetUserResp{}
	return resp, nil
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) {
	userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
		return false, fmt.Errorf("user does not exist")
	isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error {
	enpass, err := us.encryptPassword(ctx, request.Pass)
	userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
	if !has {
		return fmt.Errorf("user does not exist")
	isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
		return fmt.Errorf("the old password verification failed")
func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass))
Fixed code: 	err := uc.userService.UpdatePasswordWhenForgot(ctx, req)
	handler.HandleResponse(ctx, err, nil)
// @Param data body schema.UserModifyPasswordReq  true "UserModifyPasswordReq"
	req := &schema.UserModifyPasswordReq{}
	req.AccessToken = middleware.ExtractToken(ctx)
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
		if token == remainToken {
			continue
		}
	// user have password
	HavePassword bool `json:"have_password"`
	r.HavePassword = len(userInfo.Pass) > 0
	Avatar       *AvatarInfo `json:"avatar"`
	HavePassword bool        `json:"have_password"`
type UserModifyPasswordReq struct {
	OldPass     string `json:"old_pass"`
	Pass        string `json:"pass"`
	UserID      string `json:"-"`
	AccessToken string `json:"-"`
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
	RemoveUserTokens(ctx context.Context, userID string, remainToken string)
// 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)
	us.authService.RemoveUserAllTokens(ctx, req.UserID)
	us.authService.RemoveUserAllTokens(ctx, req.UserID)
	resp.HavePassword = len(userInfo.Pass) > 0
// UpdatePasswordWhenForgot update user password when user forgot password
func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) {
		return errors.BadRequest(reason.EmailVerifyURLExpired)
		return err
		return errors.BadRequest(reason.UserNotFound)
		return err
		return err
	// 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, req *schema.UserModifyPasswordReq) (bool, error) {
	userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID)
		return false, errors.BadRequest(reason.UserNotFound)
	isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error {
	enpass, err := us.encryptPassword(ctx, req.Pass)
	userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
	if !exist {
		return errors.BadRequest(reason.UserNotFound)

	isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
		return errors.BadRequest(reason.OldPasswordVerificationFailed)

	us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken)
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))