From b59040ff183e70828ffc741932a16ed88062560d Mon Sep 17 00:00:00 2001 From: kalac2232 <35256979+kalac2232@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:49:58 +0800 Subject: [PATCH] feat: escape HTML in user messages (#782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add HTML escaping for user messages in Markdown mode - Fix agent mention link rendering order issue - Plain mode remains unchanged (uses React elements) Tested: - User messages:
now displays as <div> - Agent mentions render correctly as clickable links in Markdown mode Co-authored-by: 郭皓楠 --- .../chat/message/parts/UserTextPart.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx index 19623ec6..41f9d473 100644 --- a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx +++ b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx @@ -19,6 +19,15 @@ const buildMentionUrl = (name: string): string => { return `https://opencode.ai/docs/agents/#${encoded}`; }; +const escapeHtml = (text: string): string => { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; + const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' => { return mode === 'markdown' ? 'markdown' : 'plain'; }; @@ -99,12 +108,18 @@ const UserTextPart: React.FC = ({ part, messageId, agentMenti }, [collapseZoneHeight, hasActiveSelectionInElement, isExpanded, isTruncated]); const processedMarkdownContent = React.useMemo(() => { - if (!agentMention?.token || !textContent.includes(agentMention.token)) { - return textContent; + let content = textContent; + + // Step 1: First escape HTML to protect against XSS and ensure HTML tags display as text + content = escapeHtml(content); + + // Step 2: Then insert agent mention links (after escaping, so tags won't be escaped) + if (agentMention?.token && content.includes(agentMention.token)) { + const mentionHtml = `${agentMention.token}`; + content = content.replace(agentMention.token, mentionHtml); } - - const mentionHtml = `${agentMention.token}`; - return textContent.replace(agentMention.token, mentionHtml); + + return content; }, [agentMention, textContent]); const plainTextContent = React.useMemo(() => {