This commit is contained in:
spaced4ndy
2026-06-29 19:08:54 +04:00
parent a926e3e5ff
commit e32dee5e65
4 changed files with 111 additions and 18 deletions
+19 -15
View File
@@ -2143,21 +2143,25 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
createContentItem gInfo Nothing Nothing
-- no delivery task - message already forwarded by relay
pure Nothing
Just m@GroupMember {memberId} -> do
(gInfo', m', scopeInfo) <- mkGetMessageChatScope cxt user gInfo m content msgScope_
if blockedByAdmin m'
then createBlockedByAdmin gInfo' (Just m') scopeInfo $> Nothing
else case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of
Just f -> rejected gInfo' (Just m') scopeInfo f $> Nothing
Nothing ->
withStore' (\db -> getCIModeration db cxt user gInfo' memberId sharedMsgId_) >>= \case
Just ciModeration -> do
applyModeration gInfo' m' scopeInfo ciModeration
withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_
pure Nothing
Nothing -> do
createContentItem gInfo' (Just m') scopeInfo
pure $ Just $ infoToDeliveryContext gInfo' scopeInfo sentAsGroup
Just m@GroupMember {memberId}
-- only an owner may post as the channel; a non-owner's signed asGroup post (e.g. relay-injected) must not render as the channel
| sentAsGroup && memberRole' m < GROwner ->
messageError "x.msg.new: member is not allowed to send as group" $> Nothing
| otherwise -> do
(gInfo', m', scopeInfo) <- mkGetMessageChatScope cxt user gInfo m content msgScope_
if blockedByAdmin m'
then createBlockedByAdmin gInfo' (Just m') scopeInfo $> Nothing
else case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of
Just f -> rejected gInfo' (Just m') scopeInfo f $> Nothing
Nothing ->
withStore' (\db -> getCIModeration db cxt user gInfo' memberId sharedMsgId_) >>= \case
Just ciModeration -> do
applyModeration gInfo' m' scopeInfo ciModeration
withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_
pure Nothing
Nothing -> do
createContentItem gInfo' (Just m') scopeInfo
pure $ Just $ infoToDeliveryContext gInfo' scopeInfo sentAsGroup
where
rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False
timed_ gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL
+4 -1
View File
@@ -105,7 +105,10 @@ batchDeliveryTasks1 _vr maxLen = toResult . foldl' addToBatch ([], [], [], 0, 0)
-- | Encode a batch element for relay groups: ><GrpMsgForward>[/<sigs>]<body>.
encodeFwdElement :: GrpMsgForward -> VerifiedMsg 'Json -> ByteString
encodeFwdElement fwd verifiedMsg = ">" <> smpEncode fwd <> encodeBatchElement signedMsg_ msgBody
encodeFwdElement fwd@GrpMsgForward {fwdSender} verifiedMsg
| FwdChannel <- fwdSender, isJust signedMsg_ =
error "encodeFwdElement: signed message must forward as FwdMember, not FwdChannel"
| otherwise = ">" <> smpEncode fwd <> encodeBatchElement signedMsg_ msgBody
where
(_, signedMsg_, msgBody) = verifiedMsgParts verifiedMsg
+2 -1
View File
@@ -33,6 +33,7 @@ import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import qualified Data.List.NonEmpty as L
import Data.Maybe (isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime, getCurrentTime)
@@ -155,7 +156,7 @@ getMsgDeliveryTask_ db taskId =
toTask ((Only taskId') :. jobScopeRow :. (senderGMId, senderMemberId, senderMemberName, brokerTs, Binary msgBody, chatBinding_, sigs_, BI showGroupAsSender)) =
case (toJobScope_ jobScopeRow, J.eitherDecodeStrict' msgBody) of
(Just jobScope, Right chatMsg) ->
let fwdSender = if showGroupAsSender then FwdChannel else FwdMember senderMemberId senderMemberName
let fwdSender = if showGroupAsSender && isNothing chatBinding_ then FwdChannel else FwdMember senderMemberId senderMemberName
-- Re-parsed from msg_body: validates stored content against current code.
-- Signed: original bytes preserved (re-encoding would invalidate signature).
-- Unsigned: re-encoded from parsed ChatMessage on forward (sanitizes content).
+86 -1
View File
@@ -36,7 +36,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), FwdSender (FwdMember), GrpMsgForward (GrpMsgForward), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), msgContentText)
import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XMsgUpdate, XMsgNew), FwdSender (FwdMember), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), mcSimple, msgContentText)
import Simplex.Chat.Types
import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation)
import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..))
@@ -334,6 +334,8 @@ chatGroupTests = do
it "should delete member message in channel" testChannelMemberMessageDelete
it "should sign member message and reuse signature on edit" testChannelMemberMessageSign
it "should reject unsigned update of a signed item" testChannelMemberUpdateEnforcement
it "should sign as-channel post and keep it displayed as the channel" testChannelAsGroupSign
it "should reject a non-owner posting as the channel" testChannelAsGroupSpoof
testGroupCheckMessages :: HasCallStack => TestParams -> IO ()
testGroupCheckMessages =
@@ -12201,6 +12203,89 @@ testChannelMemberUpdateEnforcement ps =
(Only smid : _) -> pure (SharedMsgId smid)
_ -> fail "no shared_msg_id"
testChannelAsGroupSign :: HasCallStack => TestParams -> IO ()
testChannelAsGroupSign ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath ->
withNewTestChat ps "dan" danProfile $ \dan ->
withNewTestChat ps "eve" eveProfile $ \eve -> do
createChannel1Relay "team" alice bob cath dan eve
-- owner posts as the channel, signed: verifiable AND displayed as the channel
alice ##> "/_send #1(as_group=on) sign=on text signed channel post"
alice <# "#team signed channel post"
bob <# "#team> signed channel post"
[cath, dan, eve] *<# "#team> signed channel post [>>]"
alice #$> ("/_get chat #1 count=1", chat, [(1, "signed channel post (signed)")])
cath #$> ("/_get chat #1 count=1", chat, [(0, "signed channel post (signed)")])
-- owner posts as the channel, unsigned: anonymous (FwdChannel), no signature, still as the channel
alice ##> "/_send #1(as_group=on) text plain channel post"
alice <# "#team plain channel post"
bob <# "#team> plain channel post"
[cath, dan, eve] *<# "#team> plain channel post [>>]"
alice #$> ("/_get chat #1 count=1", chat, [(1, "plain channel post")])
cath #$> ("/_get chat #1 count=1", chat, [(0, "plain channel post")])
testChannelAsGroupSpoof :: HasCallStack => TestParams -> IO ()
testChannelAsGroupSpoof ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath ->
withNewTestChat ps "dan" danProfile $ \dan ->
withNewTestChat ps "eve" eveProfile $ \eve -> do
createChannel1Relay "team" alice bob cath dan eve
promoteChannelMember "team" alice bob cath [dan, eve]
-- cath posts legitimately (introduces cath to dan as a member)
cath #> "#team hi from cath"
bob <# "#team cath> hi from cath"
concurrentlyN_
[ alice <# "#team cath> hi from cath [>>]",
do dan <### [EndsWith "updated to cath"]
dan <## "#team: bob introduced cath (Catherine) in the channel"
dan <# "#team cath> hi from cath [>>]",
do eve <### [EndsWith "updated to cath"]
eve <## "#team: bob introduced cath (Catherine) in the channel"
eve <# "#team cath> hi from cath [>>]"
]
-- the relay forges an asGroup=True post attributed to non-owner cath; dan rejects (owner guard, §2)
cathMemId <- memberIdByName bob "cath"
connId <- relayConnIdToMember bob "dan"
ts <- getCurrentTime
let ChatController {smpAgent = bobAgent} = chatController bob
container = (mcSimple (MCText "fake channel announcement")) {asGroup = Just True}
chatMsg = ChatMessage chatInitialVRange Nothing (XMsgNew container)
fwd = GrpMsgForward (FwdMember cathMemId "cath") ts
body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)]
sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)]
either (fail . show) (const $ pure ()) sent
dan <##. "error: x.msg.new: member is not allowed to send as group"
-- not rendered as the channel: dan still holds only the legitimate member message
threadDelay 1000000
dan #$> ("/_get chat #1 count=1", chat, [(0, "hi from cath")])
where
memberIdByName :: TestCC -> T.Text -> IO MemberId
memberIdByName cc name = do
rows <- withCCTransaction cc $ \db ->
DB.query db "SELECT member_id FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only ByteString]
case rows of
(Only mid : _) -> pure (MemberId mid)
_ -> fail $ "no member " <> T.unpack name
relayConnIdToMember :: TestCC -> T.Text -> IO ByteString
relayConnIdToMember cc name = do
rows <- withCCTransaction cc $ \db ->
DB.query
db
"SELECT c.agent_conn_id FROM connections c JOIN group_members m ON m.group_member_id = c.group_member_id WHERE m.local_display_name = ?"
(Only name) ::
IO [Only ByteString]
case rows of
(Only connId : _) -> pure connId
_ -> fail $ "no relay connection to member " <> T.unpack name
testGroupLinkContentFilter :: HasCallStack => TestParams -> IO ()
testGroupLinkContentFilter =
testChat3 aliceProfile bobProfile cathProfile $