From 7a3b83a660f1736e14d4ba781af2378c21ab4b71 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:43:30 +0000 Subject: [PATCH] sanitize links, include mentioned members --- src/Simplex/Chat/Web.hs | 24 +++++++++--------------- website/src/js/channel-preview.jsc | 11 ++++++----- website/src/js/simplex-lib.jsc | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Simplex/Chat/Web.hs b/src/Simplex/Chat/Web.hs index 96640cb430..cf71cffd4f 100644 --- a/src/Simplex/Chat/Web.hs +++ b/src/Simplex/Chat/Web.hs @@ -14,7 +14,6 @@ module Simplex.Chat.Web WebMemberProfile (..), WebFileInfo (..), webPreviewWorker, - renderWebPreviews, writeCorsConfig, removeStaleFiles, channelContentChanged, @@ -60,7 +59,7 @@ import Simplex.Chat.Messages ) import Simplex.Chat.Messages.CIContent (ciMsgContent) import Simplex.Chat.Protocol (MsgContent, MsgRef (..), QuotedMsg (..), isReport) -import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups, getRelayServedGroups) +import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups) import Simplex.Chat.Store.Messages (getGroupWebPreviewItems) import Simplex.Chat.Store.Shared (getGroupInfo) import Simplex.Chat.Types @@ -230,14 +229,6 @@ webPreviewWorker cfg@WebPreviewConfig {webJsonDir, webCorsFile, webUpdateInterva Right a -> pure (Just a) Left _ -> go us -renderWebPreviews :: WebPreviewConfig -> ChatController -> User -> IO () -renderWebPreviews cfg@WebPreviewConfig {webJsonDir} cc user = do - let vr' = chatVRange (config cc) - createDirectoryIfMissing True webJsonDir - groups <- withTransaction (chatStore cc) $ \db -> getRelayServedGroups db vr' user - forM_ [gInfo | gInfo <- groups, hasPublicGroup gInfo] $ \gInfo -> - void $ renderGroupPreview cfg cc user gInfo - renderGroupPreview :: WebPreviewConfig -> ChatController -> User -> GroupInfo -> IO (Maybe (Text, CorsOrigin)) renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gInfo@GroupInfo {groupProfile = gp@GroupProfile {shortDescr = sd, description = wd, publicGroup}} = case publicGroup of @@ -250,7 +241,7 @@ renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gI ts <- getCurrentTime let rendered = mapMaybe toRenderedItem $ rights items msgs = map fst rendered - senders = collectSenders $ map memberToProfile owners <> mapMaybe snd rendered + senders = collectSenders $ map memberToProfile owners <> concatMap snd rendered preview = WebChannelPreview { channel = gp, shortDescription = toFormattedText =<< sd, @@ -309,14 +300,17 @@ channelRemoved cc gId = writeTVar corsNeeded True void $ tryPutTMVar wakeSignal () -toRenderedItem :: CChatItem 'CTGroup -> Maybe (WebMessage, Maybe WebMemberProfile) +toRenderedItem :: CChatItem 'CTGroup -> Maybe (WebMessage, [WebMemberProfile]) toRenderedItem (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemTimed, itemForwarded, itemEdited}, content, mentions, formattedText, quotedItem, reactions, file}) | isJust itemTimed = Nothing | otherwise = case ciMsgContent content of Just mc | not (isReport mc) -> let (sender, senderProfile) = case chatDir of - CIGroupRcv m@GroupMember {memberId} -> (Just memberId, Just $ memberToProfile m) - _ -> (Nothing, Nothing) + CIGroupRcv m@GroupMember {memberId} -> (Just memberId, [memberToProfile m]) + _ -> (Nothing, []) + quotedProfile = case quotedItem of + Just CIQuote {chatDir = CIQGroupRcv (Just m)} -> [memberToProfile m] + _ -> [] in Just ( WebMessage { sender, @@ -330,7 +324,7 @@ toRenderedItem (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemTimed, forward = if isJust itemForwarded then Just True else Nothing, edited = itemEdited }, - senderProfile + senderProfile <> quotedProfile ) _ -> Nothing diff --git a/website/src/js/channel-preview.jsc b/website/src/js/channel-preview.jsc index 80a5aa248e..77f0be51d2 100644 --- a/website/src/js/channel-preview.jsc +++ b/website/src/js/channel-preview.jsc @@ -1185,7 +1185,7 @@ function renderMessages(container, messages, membersMap, channel) { } const col = document.createElement('div'); - const bubble = renderBubble(msg, member, showTail); + const bubble = renderBubble(msg, member, showTail, membersMap); col.appendChild(bubble); if (msg.reactions && msg.reactions.length > 0) { @@ -1198,7 +1198,7 @@ function renderMessages(container, messages, membersMap, channel) { } } -function renderBubble(msg, member, showTail) { +function renderBubble(msg, member, showTail, membersMap) { const mc = msg.content; const mediaOnly = (mc.type === 'image' || mc.type === 'video') && !mc.text && !msg.quote && !msg.forward; const noTailContent = (mc.type === 'image' || mc.type === 'video' || mc.type === 'voice') && !mc.text; @@ -1225,7 +1225,7 @@ function renderBubble(msg, member, showTail) { } if (msg.quote) { - inner.appendChild(renderQuote(msg.quote)); + inner.appendChild(renderQuote(msg.quote, membersMap)); } switch (mc.type) { @@ -1262,7 +1262,7 @@ function renderBubble(msg, member, showTail) { return bubble; } -function renderQuote(quote) { +function renderQuote(quote, membersMap) { const quoteDiv = document.createElement('div'); quoteDiv.className = 'simplex-preview-quote'; @@ -1272,7 +1272,8 @@ function renderQuote(quote) { if (quote.msgRef && quote.msgRef.memberId) { const sender = document.createElement('div'); sender.className = 'simplex-preview-quote-sender'; - sender.textContent = quote.msgRef.sent ? 'You' : (quote.msgRef.memberId || ''); + const quotedMember = membersMap[quote.msgRef.memberId]; + sender.textContent = quote.msgRef.sent ? 'You' : (quotedMember ? quotedMember.displayName : ''); contentDiv.appendChild(sender); } diff --git a/website/src/js/simplex-lib.jsc b/website/src/js/simplex-lib.jsc index b4b2d86541..4b6319c206 100644 --- a/website/src/js/simplex-lib.jsc +++ b/website/src/js/simplex-lib.jsc @@ -23,6 +23,13 @@ function escapeAttr(text) { .replace(/>/g, ">"); } +const SAFE_URI_SCHEME = /^(https?:|simplex:|mailto:|tel:)/i; + +function safeHref(uri) { + if (SAFE_URI_SCHEME.test(uri)) return escapeAttr(uri); + return `javascript:void(alert('Potentially malicious link blocked:\\n'+${JSON.stringify(uri)}))`; +} + function getSimplexLinkDescr(linkType) { switch (linkType) { case 'contact': return 'SimpleX contact address'; @@ -78,19 +85,20 @@ function renderMarkdown(fts) { case 'colored': html += `${escapeHtml(text)}`; break; - case 'uri': + case 'uri': { let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text; - html += `${escapeHtml(text)}`; + html += `${escapeHtml(text)}`; break; + } case 'hyperLink': { const { showText, linkUri } = format; - html += `${escapeHtml(showText ?? linkUri)}`; + html += `${escapeHtml(showText ?? linkUri)}`; break; } case 'simplexLink': { const { showText, linkType, simplexUri, smpHosts } = format; const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType); - html += `${linkText} (${escapeHtml(viaHost(smpHosts))})`; + html += `${linkText} (${escapeHtml(viaHost(smpHosts))})`; break; } case 'command':