VYPR
Moderate severityNVD Advisory· Published Aug 9, 2024· Updated Mar 13, 2025

Apache Answer: The link to reset the user's password will remain valid after sending a new link

CVE-2024-41890

Description

Missing Release of Resource after Effective Lifetime vulnerability in Apache Answer.

This issue affects Apache Answer: through 1.3.5.

User sends multiple password reset emails, each containing a valid link. Within the link's validity period, this could potentially lead to the link being misused or hijacked. Users are recommended to upgrade to version 1.3.6, which fixes the issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Apache Answer through 1.3.5 has a missing resource release vulnerability where multiple password reset links remain valid concurrently, allowing potential misuse.

This vulnerability in Apache Answer is a Missing Release of Resource after Effective Lifetime issue [1]. The root cause is that when a user requests a password reset, a new valid link is created but the previous one is not explicitly invalidated, so both links remain active during their validity period (e.g., 10 minutes) [3]. The code as of version 1.3.5 uses only the code string as the key when storing reset tokens; it does not tie the token to the user ID, nor does it overwrite or expire old tokens for the same user when a new reset is requested [3].

Exploitation requires an attacker to gain access to a password reset email link—for instance, through intercepted email, shared access to the user's mailbox, or a compromised email account—and then use that link to reset the user's password before the legitimate user uses the newer link [1][2]. Even if the user has already used a newer link, the older one remains valid until its time-to-live expires. This means an attacker who obtains a previously sent but still-valid link can hijack the account, as no mechanism prevents reuse of the older token once a newer one is issued [2]. The vulnerability is triggered by any user action that triggers multiple password reset emails (e.g., forgetting credentials and clicking the reset button repeatedly) [1].

The impact is that an attacker can change the victim's password and take over their account, leading to unauthorized access to the user's data and actions within the Apache Answer application [1][2]. The severity is rated as moderate, as the attacker must first get hold of a valid reset link—this is not a remote code execution or privilege escalation without authentication. However, it undermines the security guarantee that reset links should be single-use and tied to the requesting user.

The fix is implemented in Apache Answer version 1.3.6, which modifies the SetCode method to use the userID as part of the cache key, ensuring that only one reset token per user is stored and valid at any time [3]. The commit also changes the order to first save the code and then send the email, and returns early if saving fails, preventing emails with invalid tokens from being sent [3]. All users are recommended to upgrade to the latest version [1][2]. No workarounds are documented for earlier versions.

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/apache/incubator-answerGo
< 1.3.61.3.6

Affected products

2

Patches

1
2820efc454f5

feat(user): ensure that only one link is active at a time

https://github.com/apache/incubator-answerLinkinStarsJul 22, 2024via ghsa
12 files changed · +83 38
  • internal/base/constant/cache_key.go+3 0 modified
    @@ -32,6 +32,9 @@ const (
     	AdminTokenCacheKey                         = "answer:admin:token:"
     	AdminTokenCacheTime                        = 7 * 24 * time.Hour
     	UserTokenMappingCacheKey                   = "answer:user-token:mapping:"
    +	UserEmailCodeCacheKey                      = "answer:user:email-code:"
    +	UserEmailCodeCacheTime                     = 10 * time.Minute
    +	UserLatestEmailCodeCacheKey                = "answer:user-id:email-code:"
     	SiteInfoCacheKey                           = "answer:site-info:"
     	SiteInfoCacheTime                          = 1 * time.Hour
     	ConfigID2KEYCacheKeyPrefix                 = "answer:config:id:"
    
  • internal/repo/export/email_repo.go+39 4 modified
    @@ -21,6 +21,8 @@ package export
     
     import (
     	"context"
    +	"github.com/apache/incubator-answer/internal/base/constant"
    +	"github.com/tidwall/gjson"
     	"time"
     
     	"github.com/apache/incubator-answer/internal/base/data"
    @@ -42,22 +44,55 @@ func NewEmailRepo(data *data.Data) export.EmailRepo {
     }
     
     // SetCode The email code is used to verify that the link in the message is out of date
    -func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration time.Duration) error {
    -	err := e.data.Cache.SetString(ctx, code, content, duration)
    -	if err != nil {
    +func (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error {
    +	// Setting the latest code is to help ensure that only one link is active at a time.
    +	// Set userID -> latest code
    +	if err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil {
    +		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
    +	}
    +
    +	// Set latest code -> content
    +	if err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil {
     		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
     	}
     	return nil
     }
     
     // VerifyCode verify the code if out of date
     func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) {
    -	content, exist, err := e.data.Cache.GetString(ctx, code)
    +	// Get latest code -> content
    +	codeCacheKey := constant.UserEmailCodeCacheKey + code
    +	content, exist, err := e.data.Cache.GetString(ctx, codeCacheKey)
     	if err != nil {
     		return "", err
     	}
     	if !exist {
     		return "", nil
     	}
    +
    +	// Delete the code after verification
    +	_ = e.data.Cache.Del(ctx, codeCacheKey)
    +
    +	// If some email content does not need to verify the latest code is the same as the code, skip it.
    +	// For example, some unsubscribe email content does not need to verify the latest code.
    +	// This link always works before the code is out of date.
    +	if skipValidationLatestCode := gjson.Get(content, "skip_validation_latest_code").Bool(); skipValidationLatestCode {
    +		return content, nil
    +	}
    +	userID := gjson.Get(content, "user_id").String()
    +
    +	// Get userID -> latest code
    +	latestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID)
    +	if err != nil {
    +		return "", err
    +	}
    +	if !exist {
    +		return "", nil
    +	}
    +
    +	// Check if the latest code is the same as the code, if not, means the code is out of date
    +	if latestCode != code {
    +		return "", nil
    +	}
     	return content, nil
     }
    
  • internal/schema/email_template.go+2 0 modified
    @@ -42,6 +42,8 @@ type EmailCodeContent struct {
     	NotificationSources []constant.NotificationSource `json:"notification_source,omitempty"`
     	// Used for third-party login account binding
     	BindingKey string `json:"binding_key,omitempty"`
    +	// Skip the validation of the latest code
    +	SkipValidationLatestCode bool `json:"skip_validation_latest_code"`
     }
     
     func (r *EmailCodeContent) ToJSONString() string {
    
  • internal/service/content/user_service.go+4 4 modified
    @@ -227,7 +227,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
     	if err != nil {
     		return err
     	}
    -	go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
    +	go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString())
     	return nil
     }
     
    @@ -450,7 +450,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
     	if err != nil {
     		return nil, nil, err
     	}
    -	go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
    +	go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
     
     	roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
     	if err != nil {
    @@ -500,7 +500,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
     	if err != nil {
     		return err
     	}
    -	go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
    +	go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
     	return nil
     }
     
    @@ -621,7 +621,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
     	}
     	log.Infof("send email confirmation %s", verifyEmailURL)
     
    -	go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
    +	go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString())
     	return nil, nil
     }
     
    
  • internal/service/export/email_service.go+11 9 modified
    @@ -51,7 +51,7 @@ type EmailService struct {
     
     // EmailRepo email repository
     type EmailRepo interface {
    -	SetCode(ctx context.Context, code, content string, duration time.Duration) error
    +	SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error
     	VerifyCode(ctx context.Context, code string) (content string, err error)
     }
     
    @@ -89,30 +89,32 @@ func (e *EmailConfig) IsTLS() bool {
     }
     
     // SaveCode save code
    -func (es *EmailService) SaveCode(ctx context.Context, code, codeContent string) {
    -	err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute)
    +func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) {
    +	err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)
     	if err != nil {
     		log.Error(err)
     	}
     }
     
     // SendAndSaveCode send email and save code
    -func (es *EmailService) SendAndSaveCode(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) {
    -	es.Send(ctx, toEmailAddr, subject, body)
    -	err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute)
    +func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) {
    +	err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)
     	if err != nil {
     		log.Error(err)
    +		return
     	}
    +	es.Send(ctx, toEmailAddr, subject, body)
     }
     
     // SendAndSaveCodeWithTime send email and save code
     func (es *EmailService) SendAndSaveCodeWithTime(
    -	ctx context.Context, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
    -	es.Send(ctx, toEmailAddr, subject, body)
    -	err := es.emailRepo.SetCode(ctx, code, codeContent, duration)
    +	ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
    +	err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration)
     	if err != nil {
     		log.Error(err)
    +		return
     	}
    +	es.Send(ctx, toEmailAddr, subject, body)
     }
     
     // Send email send
    
  • internal/service/notification/invite_answer_notification.go+4 3 modified
    @@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx con
     		NotificationSources: []constant.NotificationSource{
     			constant.InboxSource,
     		},
    -		Email:  email,
    -		UserID: userID,
    +		Email:                    email,
    +		UserID:                   userID,
    +		SkipValidationLatestCode: true,
     	}
     
     	// If receiver has set language, use it to send email.
    @@ -74,5 +75,5 @@ func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx con
     	}
     
     	ns.emailService.SendAndSaveCodeWithTime(
    -		ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
    +		ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
     }
    
  • internal/service/notification/new_answer_notification.go+4 3 modified
    @@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx contex
     		NotificationSources: []constant.NotificationSource{
     			constant.InboxSource,
     		},
    -		Email:  email,
    -		UserID: userID,
    +		Email:                    email,
    +		UserID:                   userID,
    +		SkipValidationLatestCode: true,
     	}
     
     	// If receiver has set language, use it to send email.
    @@ -74,5 +75,5 @@ func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx contex
     	}
     
     	ns.emailService.SendAndSaveCodeWithTime(
    -		ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
    +		ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
     }
    
  • internal/service/notification/new_comment_notification.go+4 3 modified
    @@ -59,8 +59,9 @@ func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx conte
     		NotificationSources: []constant.NotificationSource{
     			constant.InboxSource,
     		},
    -		Email:  email,
    -		UserID: userID,
    +		Email:                    email,
    +		UserID:                   userID,
    +		SkipValidationLatestCode: true,
     	}
     	// If receiver has set language, use it to send email.
     	if len(lang) > 0 {
    @@ -73,5 +74,5 @@ func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx conte
     	}
     
     	ns.emailService.SendAndSaveCodeWithTime(
    -		ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
    +		ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
     }
    
  • internal/service/notification/new_question_notification.go+2 1 modified
    @@ -189,9 +189,10 @@ func (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx cont
     			constant.AllNewQuestionSource,
     			constant.AllNewQuestionForFollowingTagsSource,
     		},
    +		SkipValidationLatestCode: true,
     	}
     	ns.emailService.SendAndSaveCodeWithTime(
    -		ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
    +		ctx, userInfo.ID, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
     }
     
     func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx context.Context,
    
  • internal/service/siteinfo/siteinfo_service.go+1 1 modified
    @@ -274,7 +274,7 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
     		if err != nil {
     			return err
     		}
    -		go s.emailService.SendAndSaveCode(ctx, req.TestEmailRecipient, title, body, "", "")
    +		go s.emailService.Send(ctx, req.TestEmailRecipient, title, body)
     	}
     	return nil
     }
    
  • internal/service/user_admin/user_backyard.go+8 9 modified
    @@ -513,7 +513,7 @@ func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.
     
     func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) (
     	resp *schema.GetUserActivationResp, err error) {
    -	user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
    +	userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
     	if err != nil {
     		return nil, err
     	}
    @@ -527,11 +527,11 @@ func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.G
     	}
     
     	data := &schema.EmailCodeContent{
    -		Email:  user.EMail,
    -		UserID: user.ID,
    +		Email:  userInfo.EMail,
    +		UserID: userInfo.ID,
     	}
     	code := uuid.NewString()
    -	us.emailService.SaveCode(ctx, code, data.ToJSONString())
    +	us.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString())
     	resp = &schema.GetUserActivationResp{
     		ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code),
     	}
    @@ -540,7 +540,7 @@ func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.G
     
     // SendUserActivation send user activation email
     func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) {
    -	user, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
    +	userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID)
     	if err != nil {
     		return err
     	}
    @@ -554,17 +554,16 @@ func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.
     	}
     
     	data := &schema.EmailCodeContent{
    -		Email:  user.EMail,
    -		UserID: user.ID,
    +		Email:  userInfo.EMail,
    +		UserID: userInfo.ID,
     	}
     	code := uuid.NewString()
    -	us.emailService.SaveCode(ctx, code, data.ToJSONString())
     
     	verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code)
     	title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
     	if err != nil {
     		return err
     	}
    -	go us.emailService.SendAndSaveCode(ctx, user.EMail, title, body, code, data.ToJSONString())
    +	go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
     	return nil
     }
    
  • internal/service/user_external_login/user_external_login_service.go+1 1 modified
    @@ -328,7 +328,7 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
     	if err != nil {
     		return nil, err
     	}
    -	go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
    +	go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString())
     	return resp, nil
     }
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.