From b94c1779974e6608eab30b349e3928d412b6f0e6 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Fri, 12 Jun 2026 16:26:09 -0300 Subject: [PATCH] fix: Ignore SecureJoin messages from blocked contacts (#8295) - Ignore "vc-request-with-auth" if the sender is blocked. - Ignore SecureJoin messages on an observing device if the joiner is blocked. Even if it's a "vc-contact-confirm" message, it might be issued by another device before the joiner was blocked on the observing device. Still, to avoid membership inconsistency on devices, don't ignore "vg-member-added". - Ignore "vc-request-pubkey" if the sender is blocked by address. We don't know sender's key yet, so we should handle the sender as an address contact at this point. The sender can generate a new key and still join, but it's at least an extra work and also, in case of a group, other members will see that there's a new unknown member and be more careful. --- src/chat/chat_tests.rs | 74 ++++++++++++++++++++++++++++++++++++++++++ src/securejoin.rs | 22 +++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 200eb895d..f596d147f 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -5099,6 +5099,80 @@ async fn test_broadcast_contacts_are_hidden() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_blocked_bob_cant_join_chat() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice1 = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; + + for a in [alice1, alice2] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + // The observing device has Bob blocked from the early start. + let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await; + Contact::block(alice2, alice2_bob_id).await?; + + let alice1_chat_id = create_group(alice1, "").await?; + sync(alice1, alice2).await; + let alice1_chat = Chat::load_from_db(alice1, alice1_chat_id).await?; + let (alice2_chat_id, _blocked) = get_chat_id_by_grpid(alice2, &alice1_chat.grpid) + .await? + .unwrap(); + let qr = get_securejoin_qr(alice1, Some(alice1_chat_id)).await?; + sync(alice1, alice2).await; + + tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr) + .await; + let alice1_bob_id = alice1.add_or_lookup_contact_id(bob).await; + assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2); + // "vg-member-added" from alice1 adds bob for alice2 to provide membership consistency on + // devices. + assert_eq!(get_chat_contacts(alice2, alice2_chat_id).await?.len(), 2); + remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?; + bob.recv_msg(&alice1.pop_sent_msg().await).await; + tcm.exec_securejoin_qr(bob, alice1, &qr).await; + // Bob can join again if he isn't blocked. + assert_eq!(get_chat_contacts(alice1, alice1_chat_id).await?.len(), 2); + Contact::block(alice1, alice1_bob_id).await?; + remove_contact_from_chat(alice1, alice1_chat_id, alice1_bob_id).await?; + bob.recv_msg(&alice1.pop_sent_msg().await).await; + tcm.exec_securejoin_qr(bob, alice1, &qr).await; + let members = get_chat_contacts(alice1, alice1_chat_id).await?; + assert_eq!(members.len(), 1); + assert!(members.contains(&ContactId::SELF)); + let past_members = get_past_chat_contacts(alice1, alice1_chat_id).await?; + assert_eq!(past_members.len(), 1); + assert!(past_members.contains(&alice1_bob_id)); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_blocked_bob_cant_create_11_chat_via_securejoin() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice1 = &tcm.alice().await; + let alice2 = &tcm.alice().await; + let bob = &tcm.bob().await; + + for a in [alice1, alice2] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + // The observing device has Bob blocked. + let alice2_bob_id = alice2.add_or_lookup_contact_id(bob).await; + Contact::block(alice2, alice2_bob_id).await?; + + let qr = get_securejoin_qr(alice1, None).await?; + sync(alice1, alice2).await; + + let chat_cnt = get_chat_cnt(alice1).await?; + assert_eq!(get_chat_cnt(alice2).await?, chat_cnt); + tcm.exec_securejoin_qr_multi_device(bob, &[alice1, alice2], &qr) + .await; + assert_eq!(get_chat_cnt(alice1).await?, chat_cnt + 1); + assert_eq!(get_chat_cnt(alice2).await?, chat_cnt); + Ok(()) +} + /// Tests sending JPEG image with .png extension. /// /// This is a regression test, previously sending failed diff --git a/src/securejoin.rs b/src/securejoin.rs index 117c963ba..068bf0fa1 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -533,6 +533,18 @@ pub(crate) async fn handle_securejoin_handshake( warn!(context, "Secure-join denied (bad auth)."); return Ok(HandshakeMessage::Ignore); } + if Contact::lookup_id_by_addr_ex( + context, + &mime_message.from.addr, + Origin::Unknown, + Some(Blocked::Yes), + ) + .await? + .is_some() + { + warn!(context, "Ignoring {step} message: {contact_id} is blocked."); + return Ok(HandshakeMessage::Ignore); + } let rfc724_mid = create_outgoing_rfc724_mid(); let addr = ContactAddress::new(&mime_message.from.addr)?; @@ -640,6 +652,10 @@ pub(crate) async fn handle_securejoin_handshake( if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS { mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?; } + if sender_contact.blocked { + warn!(context, "Ignoring {step} message: {contact_id} is blocked."); + return Ok(HandshakeMessage::Ignore); + } contact_id.regossip_keys(context).await?; // for setup-contact, make Alice's one-to-one chat with Bob visible // (secure-join-information are shown in the group chat) @@ -811,6 +827,12 @@ pub(crate) async fn observe_securejoin_on_other_device( } mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?; + if contact.blocked && step != SecureJoinStep::MemberAdded { + // Contact might be blocked after another device had issued the message. Still, to avoid + // membership inconsistency on devices, don't ignore "vg-member-added". + warn!(context, "Observing {step}: {contact_id} is blocked."); + return Ok(HandshakeMessage::Ignore); + } if matches!( step,