Apache Answer: The link for resetting user password is not Single-Use
Description
Missing Release of Resource after Effective Lifetime vulnerability in Apache Answer.
This issue affects Apache Answer: through 1.3.5.
The password reset link remains valid within its expiration period even after it has been used. 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 password reset links remain valid after use within expiration, allowing reuse for account takeover; fixed in version 1.3.6.
The vulnerability in Apache Answer (through version 1.3.5) is a Missing Release of Resource after Effective Lifetime (CWE-772) affecting password reset functionality. The password reset link is not invalidated after being used; it remains valid within its expiration period (default 10 minutes), allowing reuse.
An attacker who gains access to a legitimate password reset link—for example, by intercepting the email, accessing a shared mailbox, or through a man-in-the-middle attack—can reuse the same link to reset the user's password again. No additional authentication is required beyond the link itself.
Successful exploitation enables an attacker to change the victim's password, leading to full account takeover. This could result in unauthorized access to sensitive information or further compromise of the application.
The issue is fixed in Apache Answer version 1.3.6. The commit [3] modifies the email service to include the user ID in the cache key and invalidates existing codes when a new reset request is made, ensuring single-use semantics. Users are strongly advised to upgrade to the latest version [1][2].
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/apache/incubator-answerGo | < 1.3.6 | 1.3.6 |
Affected products
2- Apache Software Foundation/Apache Answerv5Range: 0
Patches
12820efc454f5feat(user): ensure that only one link is active at a time
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- github.com/advisories/GHSA-v3x9-wrq5-868jghsaADVISORY
- lists.apache.org/thread/jbs1j2o9rqm5sc19jyk3jcfvkmfkmyf4ghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2024-41888ghsaADVISORY
- github.com/apache/incubator-answerghsaPACKAGE
- www.openwall.com/lists/oss-security/2024/08/09/5ghsaWEB
- github.com/apache/incubator-answer/commit/2820efc454f5808974dce0aa99aac106be3f727bghsaWEB
News mentions
0No linked articles in our index yet.