CVE-2026-34033
Description
Apache Answer through 2.0.0 is vulnerable to XSS, allowing authenticated users to inject HTML into notification emails.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Answer through 2.0.0 is vulnerable to XSS, allowing authenticated users to inject HTML into notification emails.
Vulnerability
An Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS) vulnerability exists in Apache Answer through version 2.0.0. User-supplied content was included in notification emails without proper escaping, leading to this issue [1].
Exploitation
An attacker must be an authenticated user within Apache Answer. They can then inject arbitrary HTML into notification emails that are sent to other users [1].
Impact
Successful exploitation allows an authenticated attacker to inject arbitrary HTML into notification emails sent to other users. This could lead to cross-site scripting (XSS) attacks within the email client or other downstream processing of the email content [1].
Mitigation
Users are recommended to upgrade to Apache Answer version 2.0.1, which addresses this issue. The fixed version was released on June 9, 2026 [1].
AI Insight generated on Jun 9, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
3d1a4092c61ccfix(email): enhance email templates by escaping HTML characters in dynamic content
1 file changed · +35 −4
internal/service/export/email_service.go+35 −4 modified@@ -23,6 +23,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "html" "mime" "os" "strings" @@ -224,6 +225,10 @@ func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, e return title, body, nil } +func escapeEmailHTMLText(text string) string { + return html.EscapeString(text) +} + // NewAnswerTemplate new answer template func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) ( title, body string, err error) { @@ -246,7 +251,14 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) - body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, &schema.NewAnswerTemplateData{ + SiteName: escapeEmailHTMLText(templateData.SiteName), + DisplayName: escapeEmailHTMLText(templateData.DisplayName), + QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle), + AnswerUrl: templateData.AnswerUrl, + AnswerSummary: escapeEmailHTMLText(templateData.AnswerSummary), + UnsubscribeUrl: templateData.UnsubscribeUrl, + }) return title, body, nil } @@ -271,7 +283,13 @@ func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData) - body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, &schema.NewInviteAnswerTemplateData{ + SiteName: escapeEmailHTMLText(templateData.SiteName), + DisplayName: escapeEmailHTMLText(templateData.DisplayName), + QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle), + InviteUrl: templateData.InviteUrl, + UnsubscribeUrl: templateData.UnsubscribeUrl, + }) return title, body, nil } @@ -298,7 +316,14 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) - body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, &schema.NewCommentTemplateData{ + SiteName: escapeEmailHTMLText(templateData.SiteName), + DisplayName: escapeEmailHTMLText(templateData.DisplayName), + QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle), + CommentUrl: templateData.CommentUrl, + CommentSummary: escapeEmailHTMLText(templateData.CommentSummary), + UnsubscribeUrl: templateData.UnsubscribeUrl, + }) return title, body, nil } @@ -324,7 +349,13 @@ func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.New lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData) - body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, &schema.NewQuestionTemplateData{ + SiteName: escapeEmailHTMLText(templateData.SiteName), + QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle), + QuestionUrl: templateData.QuestionUrl, + Tags: escapeEmailHTMLText(templateData.Tags), + UnsubscribeUrl: templateData.UnsubscribeUrl, + }) return title, body, nil }
869b040e92d6fix(auth): enhance admin user cache management and add status checks for email verification and suspension
2 files changed · +48 −1
internal/base/middleware/auth.go+15 −0 modified@@ -184,7 +184,22 @@ func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc { return } if userInfo != nil { + if userInfo.EmailStatus == entity.EmailStatusToBeVerified { + _ = am.authService.RemoveAdminUserCacheInfo(ctx, token) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive}) + ctx.Abort() + return + } + if userInfo.UserStatus == entity.UserStatusSuspended { + _ = am.authService.RemoveAdminUserCacheInfo(ctx, token) + handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended}) + ctx.Abort() + return + } if userInfo.UserStatus == entity.UserStatusDeleted { + _ = am.authService.RemoveAdminUserCacheInfo(ctx, token) handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) ctx.Abort() return
internal/service/auth/auth.go+33 −1 modified@@ -145,7 +145,39 @@ func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID // Admin func (as *AuthService) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { - return as.authRepo.GetAdminUserCacheInfo(ctx, accessToken) + adminCacheInfo, err := as.authRepo.GetAdminUserCacheInfo(ctx, accessToken) + if err != nil { + return nil, err + } + if adminCacheInfo == nil { + return nil, nil + } + + // Keep admin authorization aligned with user-token lifecycle and status refresh. + refreshedUserCacheInfo, err := as.GetUserCacheInfo(ctx, accessToken) + if err != nil { + return nil, err + } + if refreshedUserCacheInfo == nil { + if err = as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken); err != nil { + return nil, err + } + return nil, nil + } + + adminCacheInfo.UserStatus = refreshedUserCacheInfo.UserStatus + adminCacheInfo.EmailStatus = refreshedUserCacheInfo.EmailStatus + if refreshedUserCacheInfo.RoleID > 0 { + adminCacheInfo.RoleID = refreshedUserCacheInfo.RoleID + } + if len(refreshedUserCacheInfo.ExternalID) > 0 { + adminCacheInfo.ExternalID = refreshedUserCacheInfo.ExternalID + } + + if err = as.authRepo.SetAdminUserCacheInfo(ctx, accessToken, adminCacheInfo); err != nil { + return nil, err + } + return adminCacheInfo, nil } func (as *AuthService) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {
0db88d63e304fix(chat): implement HTML rendering for display content
3 files changed · +65 −9
ui/src/components/BubbleAi/index.tsx+61 −5 modified@@ -21,10 +21,9 @@ import { FC, useEffect, useState, useRef } from 'react'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { marked } from 'marked'; import copy from 'copy-to-clipboard'; -import { voteConversation } from '@/services'; +import { markdownToHtml, voteConversation } from '@/services'; import { Icon, htmlRender } from '@/components'; interface IProps { @@ -40,6 +39,17 @@ interface IProps { }; } +const escapeHtml = (text: string) => + text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +const renderPlainTextAsHtml = (text: string) => + escapeHtml(text).replace(/\r?\n/g, '<br />'); + const BubbleAi: FC<IProps> = ({ canType = false, isLast, @@ -55,6 +65,7 @@ const BubbleAi: FC<IProps> = ({ const [isHelpful, setIsHelpful] = useState(false); const [isUnhelpful, setIsUnhelpful] = useState(false); const [canShowAction, setCanShowAction] = useState(false); + const [safeHtml, setSafeHtml] = useState(''); const typewriterRef = useRef<{ timer: NodeJS.Timeout | null; index: number; @@ -64,6 +75,8 @@ const BubbleAi: FC<IProps> = ({ index: 0, isTyping: false, }); + const renderTimerRef = useRef<NodeJS.Timeout | null>(null); + const renderTaskRef = useRef(0); const fmtContainer = useRef<HTMLDivElement>(null); // add ref for ScrollIntoView const containerRef = useRef<HTMLDivElement>(null); @@ -194,13 +207,56 @@ const BubbleAi: FC<IProps> = ({ }; }, [content, isCompleted]); + useEffect(() => { + if (renderTimerRef.current) { + clearTimeout(renderTimerRef.current); + renderTimerRef.current = null; + } + renderTaskRef.current += 1; + const currentRenderTask = renderTaskRef.current; + + if (!displayContent) { + setSafeHtml(''); + return undefined; + } + + // During streaming, render escaped plain text to avoid executing unsanitized HTML. + if (!isCompleted) { + setSafeHtml(renderPlainTextAsHtml(displayContent)); + return undefined; + } + + renderTimerRef.current = setTimeout(() => { + markdownToHtml(displayContent) + .then((resp) => { + if (renderTaskRef.current !== currentRenderTask) { + return; + } + setSafeHtml(resp || renderPlainTextAsHtml(displayContent)); + }) + .catch(() => { + if (renderTaskRef.current !== currentRenderTask) { + return; + } + setSafeHtml(renderPlainTextAsHtml(displayContent)); + }); + }, 0); + + return () => { + if (renderTimerRef.current) { + clearTimeout(renderTimerRef.current); + renderTimerRef.current = null; + } + }; + }, [displayContent, isCompleted]); + useEffect(() => { setIsHelpful(actionData.helpful > 0); setIsUnhelpful(actionData.unhelpful > 0); }, [actionData]); useEffect(() => { - if (fmtContainer.current && isCompleted) { + if (fmtContainer.current && isCompleted && safeHtml) { htmlRender(fmtContainer.current, { copySuccessText: t('copied', { keyPrefix: 'messages' }), copyText: t('copy', { keyPrefix: 'messages' }), @@ -211,7 +267,7 @@ const BubbleAi: FC<IProps> = ({ }); setCanShowAction(true); } - }, [isCompleted, fmtContainer.current]); + }, [isCompleted, safeHtml, t]); return ( <div @@ -223,7 +279,7 @@ const BubbleAi: FC<IProps> = ({ className="fmt text-break text-wrap" ref={fmtContainer} style={{ transition: 'all 0.2s ease' }} - dangerouslySetInnerHTML={{ __html: marked.parse(displayContent) }} + dangerouslySetInnerHTML={{ __html: safeHtml }} /> {canShowAction && (
ui/src/pages/AiAssistant/index.tsx+1 −1 modified@@ -328,7 +328,7 @@ const Index = () => { canType={isGenerate && isLastMessage} chatId={item.chat_completion_id} isLast={isLastMessage} - isCompleted={!isGenerate} + isCompleted={!isGenerate || !isLastMessage} content={item.content} actionData={{ helpful: item.helpful,
ui/src/pages/Search/components/AiCard/index.tsx+3 −3 modified@@ -149,10 +149,10 @@ const Index = () => { <BubbleUser content={item.content} /> ) : ( <BubbleAi - canType + canType={isGenerate && isLastMessage} chatId={item.chat_completion_id} - isLast - isCompleted={!isGenerate} + isLast={isLastMessage} + isCompleted={!isGenerate || !isLastMessage} content={item.content} actionData={{ helpful: item.helpful,
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
2News mentions
0No linked articles in our index yet.