CVE-2026-25688
Description
Apache Answer through 2.0.0 is vulnerable to XSS, allowing script execution when AI-generated content is viewed.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Answer through 2.0.0 is vulnerable to XSS, allowing script execution when AI-generated content is viewed.
Vulnerability
Improper neutralization of alternate XSS syntax in Apache Answer through version 2.0.0 allows for malicious script execution. This occurs when AI-generated response content is rendered in the browser without proper sanitization [1].
Exploitation
An attacker can exploit this vulnerability by crafting malicious AI-generated content that, when viewed by a user, executes arbitrary scripts within the user's browser context. No specific authentication or user interaction beyond viewing the content is mentioned in the available references.
Impact
Successful exploitation allows an attacker to execute arbitrary scripts in the context of the victim's browser session. This could lead to session hijacking, data theft, or further malicious actions within the application, depending on the user's privileges and the application's functionality.
Mitigation
Apache Answer version 2.0.1 addresses this vulnerability. Users are recommended to upgrade to version 2.0.1 or later. The release date for version 2.0.1 is not specified in the provided references [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
20db88d63e304fix(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,
d1a4092c61ccfix(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 }
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
2News mentions
0No linked articles in our index yet.