From 1bf57d40984f4270b2eb676825eb79ae65088d30 Mon Sep 17 00:00:00 2001 From: shum Date: Fri, 2 Oct 2026 11:18:29 +0000 Subject: [PATCH] core: check forwarding relay at dispatch --- src/Simplex/Chat/Library/Subscriber.hs | 15 +++++++++----- tests/ChatTests/Groups.hs | 27 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index bac8ae4da8..9eaf95434b 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1029,8 +1029,9 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag case fwd_ of Just fwd | SJson <- enc -> do logInfo $ "group fwd=" <> tshow tag <> " " <> eInfo - xGrpMsgForward (GIK gInfo' gks) scopeInfo m' fwd parsedMsg brokerTs - `catchAllErrors` \e -> eToView e + withGrpFwdRelay gInfo' m' $ + xGrpMsgForward (GIK gInfo' gks) scopeInfo m' fwd parsedMsg brokerTs + `catchAllErrors` \e -> eToView e pure newDeliveryTasks -- direct JSON and binary messages; binary events don't produce delivery tasks _ -> do @@ -1105,7 +1106,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest (GIK gInfo' gks) m'' reqVer -- TODO [knocking] why don't we forward these messages? XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs - XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward (GIK gInfo' gks) Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs + XGrpMsgForward fwd msg' -> Nothing <$ withGrpFwdRelay gInfo' m'' (xGrpMsgForward (GIK gInfo' gks) Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs) XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe @@ -3758,6 +3759,11 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag | useRelays' gInfo = isRelay m | otherwise = memberRole' m >= GRAdmin + withGrpFwdRelay :: GroupInfo -> GroupMember -> CM () -> CM () + withGrpFwdRelay gInfo m action + | isMemberGrpFwdRelay gInfo m = action + | otherwise = messageError "x.grp.msg.forward: forwarding member is not a relay" + unknownMemberRole :: GroupInfo -> CM GroupMemberRole unknownMemberRole gInfo | useRelays' gInfo = asks $ channelSubscriberRole . config @@ -3928,8 +3934,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing xGrpMsgForward :: GroupInfoKeys -> Maybe GroupChatScopeInfo -> GroupMember -> GrpMsgForward -> ParsedMsg 'Json -> UTCTime -> CM () - xGrpMsgForward g@(GIK gInfo _) scopeInfo m@GroupMember {localDisplayName} GrpMsgForward {fwdSender, fwdBrokerTs = msgTs} parsedMsg@(ParsedMsg _ _ chatMsg@ChatMessage {chatMsgEvent}) brokerTs = do - unless (isMemberGrpFwdRelay gInfo m) $ throwChatError (CEGroupContactRole localDisplayName) + xGrpMsgForward g@(GIK gInfo _) scopeInfo m GrpMsgForward {fwdSender, fwdBrokerTs = msgTs} parsedMsg@(ParsedMsg _ _ chatMsg@ChatMessage {chatMsgEvent}) brokerTs = case fwdSender of FwdMember memberId memberName -> do unknownRole <- unknownMemberRole gInfo diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 0cc697b012..b48569b811 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -38,7 +38,7 @@ import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) import Simplex.Chat.Messages.Batch (encodeBinaryBatch, encodeFwdElement) import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText) import Simplex.Chat.Options -import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XMsgUpdate, XMsgNew, XMsgDel), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), mcSimple, msgContentText) +import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XGrpMsgForward, XMsgUpdate, XMsgNew, XMsgDel), EncodedChatMessage (..), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), encodeChatMessage, maxEncodedMsgLength, mcSimple, msgContentText) import Simplex.Chat.Types import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation) import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..)) @@ -189,6 +189,7 @@ chatGroupTests = do it "forward member removal (x.grp.mem.del)" testGroupMsgForwardMemberRemoval it "forward admin removal (x.grp.mem.del, relay forwards it was removed)" testGroupMsgForwardAdminRemoval it "forward group deletion (x.grp.del)" testGroupMsgForwardGroupDeletion + it "reject forwarded messages from member without forwarding role (x.grp.msg.forward)" testGroupMsgForwardFromMemberRejected describe "group history" $ do it "text messages" testGroupHistory it "history is sent when joining via group link" testGroupHistoryGroupLink @@ -5867,6 +5868,30 @@ testGroupMsgForwardGroupDeletion = cath ##> "/groups" cath <## "#team (group deleted, delete local copy: /d #team)" +testGroupMsgForwardFromMemberRejected :: HasCallStack => TestParams -> IO () +testGroupMsgForwardFromMemberRejected = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) + aliceMemId <- memberIdByName cath "alice" + connId <- relayConnIdToMember cath "bob" + ts <- getCurrentTime + let ChatController {smpAgent = cathAgent} = chatController cath + fwd = GrpMsgForward (FwdMember aliceMemId "alice") ts + forgedMsg text = ChatMessage chatInitialVRange Nothing (XMsgNew $ mcSimple (MCText text)) + sendBody body = do + sent <- runExceptT $ sendMessages cathAgent [(connId, PQEncOff, MsgFlags False, vrValue body)] + either (fail . show) (const $ pure ()) sent + sendBody $ encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned $ forgedMsg "forged element")] + bob <## "error: x.grp.msg.forward: forwarding member is not a relay" + case encodeChatMessage maxEncodedMsgLength (ChatMessage chatInitialVRange Nothing (XGrpMsgForward fwd $ forgedMsg "forged event")) of + ECMEncoded body -> sendBody body + ECMLarge -> fail "x.grp.msg.forward exceeds maximum message size" + bob <## "error: x.grp.msg.forward: forwarding member is not a relay" + bob #$> ("/_get chat #1 count=100 search=forged", chat, []) + cath #> "#team hello" + [alice, bob] *<# "#team cath> hello" + testGroupHistory :: HasCallStack => TestParams -> IO () testGroupHistory = testChat3 aliceProfile bobProfile cathProfile $