CVE-2023-4126 (published 2023-08-03T04:15Z)

Insufficient Session Expiration in GitHub repository answerdev/answer prior to v1.1.0.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-613
cwe_name: Insufficient Session Expiration
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."
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/answerdev/answer
commit_details: repo_url: https://github.com/answerdev/answer
author: LinkinStars
author_date: 2023-05-08 17:51:29+08:00
msg: feat(password): logout other user when update password
repository: repo_url: https://github.com/answerdev/answer
repo_name: apache/incubator-answer
description: 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.
repo_language: Go
owner: answerdev
file_changes: filename: user_controller.go
change_type: ModificationType.MODIFY
diff: @@ -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 {

programming_language: Go
filename: auth.go
change_type: ModificationType.MODIFY
diff: @@ -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 {

programming_language: Go
filename: user_schema.go
change_type: ModificationType.MODIFY
diff: @@ -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 {

programming_language: Go
filename: auth.go
change_type: ModificationType.MODIFY
diff: @@ -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

programming_language: Go
filename: user_backyard.go
change_type: ModificationType.MODIFY
diff: @@ -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
 }
 

programming_language: Go
filename: user_service.go
change_type: ModificationType.MODIFY
diff: @@ -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
 }
 

programming_language: Go