Skip to content
This repository was archived by the owner on Aug 5, 2026. It is now read-only.

Fix blocked welcome sender checks - #848

Draft
mubarakcoded wants to merge 2 commits into
masterfrom
fix/welcome-blocked-welcomer-105
Draft

Fix blocked welcome sender checks#848
mubarakcoded wants to merge 2 commits into
masterfrom
fix/welcome-blocked-welcomer-105

Conversation

@mubarakcoded

@mubarakcoded mubarakcoded commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • use the verified NIP-59 seal sender for the pre-MLS welcome block check instead of the unsigned rumor pubkey
  • re-check MLS-authenticated welcome identities before saving app-visible group state or accepting the welcome
  • add regression coverage for both blocked seal sender and blocked MLS welcomer paths

Closes marmot-protocol/marmot-security#105

Verification

  • cargo fmt --check
  • cargo test -p whitenoise test_handle_giftwrap_welcome_blocked --lib

Note: just precommit-quick was also attempted; fmt/docs/clippy/dead_code passed, but the full unit test phase hit existing relay-dependent localhost failures unrelated to this change.


Open in Stage

Summary by CodeRabbit

  • Bug Fixes

    • Welcome processing now rejects welcomes early if the gift sender or any welcomer/admin identity (excluding the recipient) is blocked, preventing unwanted group joins and cleaning up pending group state.
  • Tests

    • Added regression tests for blocked sender and blocked welcomer (including decoy cases).
    • Added a test helper to create accounts with active sessions and secrets-store state; strengthened welcome assertions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

handle_giftwrap now routes the giftwrap's verified sender (unwrapped.sender) as seal_sender into process_welcome, which performs an early block check on that cryptographic identity. After MLS processing, it checks whether the welcomer or group admins are blocked, dropping the welcome if so. Tests verify both the giftwrap signer and injected welcomer blocking paths.

Changes

Welcome block/mute verification

Layer / File(s) Summary
Seal verification and welcome identity block checks
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs
Import BTreeSet for block checks. Pass seal_sender (verified giftwrap signer) from handle_giftwrap into process_welcome signature. Replace early block check to use seal_sender instead of unsigned rumor.pubkey. After MLS welcome processing, build a set of welcome identities (welcomer + group admin pubkeys) and drop the welcome if any are blocked.
Test helpers and regression tests
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs
Add test imports for mute-list and session utilities. Create create_account_with_session helper to set up accounts with persisted secrets-store state and MLS signer. Tighten existing success-path assertion. Add two regression tests: one verifying that a blocked seal_sender causes the welcome to be dropped, and one verifying that a blocked welcomer identity (injected via decoy rumor) causes the welcome to be dropped.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • marmot-protocol/marmot-security#105 — Implements the requested fixes: stop trusting unsigned rumor.pubkey, use verified seal_sender early, and re-check MLS-authenticated welcomer/admin identities to prevent block/mute bypass.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fixing blocked welcome sender checks by using verified seal sender instead of unsigned rumor.pubkey.
Linked Issues check ✅ Passed The PR correctly implements all required objectives from issue #105: uses verified seal sender for pre-MLS check, re-checks MLS-authenticated welcome identities post-MLS, and includes regression tests validating both blocked seal sender and blocked welcomer paths.
Out of Scope Changes check ✅ Passed All changes are within scope: block check logic refinements, test additions covering the security bypass, and test helper improvements directly address the security vulnerability described in issue #105.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/welcome-blocked-welcomer-105

Comment @coderabbitai help to get the list of available commands and usage tips.

@ghost

ghost commented May 25, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 3 individual chapters for you:

Title
1 Pass seal sender to welcome processing
2 Implement multi-layer block checks for welcomes
3 Add regression tests for blocked welcomes
Open in Stage

Chapters generated by Stage for commit d660299 on May 25, 2026 1:14pm UTC.

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown

✅ Coverage: 94.08% → 94.09% (+0.01%)

History
  • 3ae1454 2026-05-25 13:05:24 UTC — 94.09% (+0.01% vs base)
  • bd6019a 2026-05-25 13:25:07 UTC — 94.09% (+0.01% vs base)

@mubarakcoded

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

@mubarakcoded — full review of PR #848 below.


PR Review: Fix blocked welcome sender checks

Summary: The core security fix is correct and well-reasoned — replacing the attacker-controllable rumor.pubkey with the cryptographically-verified UnwrappedGift::sender for the pre-MLS check, and adding a post-MLS check against the MLS-authenticated welcome.welcomer and group_admin_pubkeys. The ordering is correct: both checks complete before account_group.save() and mdk.accept_welcome(), so no app-visible state leaks to blocked users.


🔴 High — Orphaned MDK state when post-MLS check drops the welcome

mdk.process_welcome() is called before the post-MLS identity check. When a blocked welcomer or admin triggers the drop, mdk.accept_welcome() is never called, but neither is any rejection/cleanup API:

// process_welcome creates internal MDK state here:
let welcome = mdk
    .process_welcome(&event.id, &rumor)
    .map_err(WhitenoiseError::MdkCoreError)?;

// ... post-MLS block check ...
if session.mute_list().is_user_blocked(&pubkey).await? {
    return Ok(());  // MDK state from process_welcome is left orphaned
}

// Only reached for un-blocked welcomes:
mdk.accept_welcome(&welcome)?;

If MDK tracks pending-welcome state keyed by event ID (e.g. to prevent double-processing), a relay replay of the same giftwrap after the initial drop could hit a "already processed" error path rather than being silently dropped again. Depending on mdk_core's contract for process_welcome without a paired accept_welcome, this may cause unexpected behavior.

Recommendation: Check whether mdk_core exposes a reject_welcome or similar cleanup API. If so, call it in the early-return path. If the current MDK contract is that un-accepted welcomes are simply GC'd, add a comment making that explicit:

// NOTE: mdk.process_welcome() was called but mdk.accept_welcome() is intentionally
// skipped for blocked welcomers. MDK treats un-accepted welcomes as ephemeral —
// no persistent state is written. Re-delivery of this welcome will be dropped again
// by the same block check.
return Ok(());

🟡 Medium — Missing explanatory comment on the early seal_sender check

The original code had a comment explaining the rationale; the replacement code removed it entirely:

// Before (removed):
// Reject welcomes from blocked users before any MLS processing.
// Using `rumor.pubkey` (the Nostr identity of the sender) avoids
// allocating MDK group state or writing any DB rows for blocked senders.

// After (no comment):
if session.mute_list().is_user_blocked(&seal_sender).await? {

Future readers won't know why seal_sender is used here versus rumor.pubkey (the security distinction). Consider:

// Cheap pre-MLS block check using the cryptographically-verified seal author
// (UnwrappedGift::sender). Unlike rumor.pubkey, which is unsigned and
// attacker-controllable per NIP-59, seal_sender is the verified giftwrap signer.
// This avoids allocating MDK group state for welcomes from blocked users.
if session.mute_list().is_user_blocked(&seal_sender).await? {

🟡 Medium — N+1 async is_user_blocked calls

The post-MLS loop issues a separate DB round-trip per identity:

for pubkey in welcome_identities {
    if pubkey == account.pubkey { continue; }
    if session.mute_list().is_user_blocked(&pubkey).await? {  // one DB call per pubkey
        return Ok(());
    }
}

In a group with many admins this becomes N sequential awaits. If MuteList supports a bulk/set lookup, prefer that. If not, this is a reasonable follow-up to file.


🟡 Medium — BTreeSet used where HashSet suffices

Iteration order over PublicKey values is irrelevant here; BTreeSet adds unnecessary sort-key comparisons. If PublicKey implements Hash (standard in nostr_sdk), prefer:

use std::collections::HashSet;
// ...
let mut welcome_identities = HashSet::from([welcomer_pubkey]);
welcome_identities.extend(welcome.group_admin_pubkeys.iter().copied());

🟢 Low — Test coverage gap: blocked non-welcomer admin

Both regression tests block the seal sender or the welcomer. A third case — a welcome where the welcomer is not blocked but an admin is — is untested. Consider:

#[tokio::test]
async fn test_handle_giftwrap_welcome_blocked_admin_is_dropped() {
    // creator is NOT blocked, but an admin injected into group_admin_pubkeys IS blocked
    // ...
    assert!(visible_groups.is_empty(), "blocked admin must not create a visible account group");
}

🟢 Nit — process_welcome signature has no doc comment update

The seal_sender: PublicKey parameter was added without updating (or adding) a /// doc comment. For a pub(crate) or async fn with security relevance, a short doc note on parameter semantics aids audits.


✅ What's done well

  • Ordering is correct: both block checks complete before account_group.save() and mdk.accept_welcome(), preventing any app-visible state from leaking to blocked users.
  • The decoy_rumor test correctly validates the NIP-59 threat model (attacker spoofs rumor.pubkey but cannot spoof UnwrappedGift::sender or MLS-authenticated identities).
  • assert!(result.is_ok(), "Expected Ok, got: {:?}", result) improvement is a good DX change.
  • create_account_with_session helper reduces test boilerplate cleanly.
  • The self-skip (if pubkey == account.pubkey { continue; }) correctly prevents the account from blocking itself.
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)

149-176: ⚠️ Potential issue | 🟠 Major

Add pending-welcome discard/reject on blocked welcomer/admin early return
In src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (the early return Ok(()) after mdk.process_welcome(...) for blocked welcomer/admin identities), mdk-core’s process_welcome persists the incoming welcome as “pending welcomes” (pending_welcomes). That path should explicitly call the mdk-core pending-welcome reject/discard cleanup API (from crates/mdk-core/src/welcomes.rs) so dropped welcomes don’t accumulate.

  • Extend the regression tests to assert the pending-welcome state is empty/not retained for these dropped cases (not just that AccountGroup::visible_for_account(...) is empty).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs` around
lines 149 - 176, The early return inside the loop in handle_giftwrap after
calling mdk.process_welcome(...) can leave the welcome persisted in mdk-core’s
pending_welcomes; before returning from the blocked-welcomer/admin branch (the
branch that checks session.mute_list().is_user_blocked(...) and currently does
return Ok(())) call the mdk-core pending-welcome cleanup API (the reject/discard
function exposed in crates::mdk_core::welcomes.rs) to explicitly reject/discard
the pending welcome for that event ID (use the event.id or welcome metadata to
identify the pending welcome), then return; also update the existing regression
tests to assert that mdk-core’s pending_welcomes is empty/not retaining the
dropped welcome (in addition to asserting AccountGroup::visible_for_account(...)
remains empty).
🧹 Nitpick comments (1)
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)

859-937: ⚡ Quick win

Add a regression that isolates the blocked-admin branch.

The new post-MLS guard also walks welcome.group_admin_pubkeys, but this test blocks the creator/welcomer. A case where the welcomer is allowed and only an injected admin is blocked would lock down the admin-path logic separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs` around
lines 859 - 937, The test
test_handle_giftwrap_welcome_blocked_welcomer_with_decoy_rumor_is_dropped
currently mutes the creator/welcomer
(MuteListEntry::insert(&creator_account.pubkey,...)) which walks
welcome.group_admin_pubkeys too; change or add a new regression test that
instead leaves the welcomer allowed and mutes an injected admin key from
welcome.group_admin_pubkeys so the admin-path is exercised independently.
Concretely: create a separate admin Keys (e.g., admin_keys), include
admin_pubkey in the group config via create_nostr_group_config_data so it
appears in welcome.group_admin_pubkeys, call
MuteListEntry::insert(&admin_pubkey, false,
&member_session.account_db).await.unwrap() (not the creator), then build the
same giftwrap via EventBuilder::gift_wrap and call handle_giftwrap; assert the
giftwrap is dropped and AccountGroup::visible_for_account still returns empty.
Ensure the new test name reflects "blocked_admin" to isolate the admin branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs`:
- Around line 850-856: The tests currently only assert
AccountGroup::visible_for_account(...) is empty which can miss cases where a
welcome was accepted later; update both test blocks (around the
visible_for_account checks at the shown diff and also at the other location) to
additionally assert the account's pending/group state is empty by calling
create_mdk_for_account(...).get_groups().unwrap().is_empty() (or the equivalent
pending-invite check) for the same member_account.pubkey, ensuring that no MLS
groups exist even in the MDK (i.e., the welcome was never accepted).

---

Outside diff comments:
In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs`:
- Around line 149-176: The early return inside the loop in handle_giftwrap after
calling mdk.process_welcome(...) can leave the welcome persisted in mdk-core’s
pending_welcomes; before returning from the blocked-welcomer/admin branch (the
branch that checks session.mute_list().is_user_blocked(...) and currently does
return Ok(())) call the mdk-core pending-welcome cleanup API (the reject/discard
function exposed in crates::mdk_core::welcomes.rs) to explicitly reject/discard
the pending welcome for that event ID (use the event.id or welcome metadata to
identify the pending welcome), then return; also update the existing regression
tests to assert that mdk-core’s pending_welcomes is empty/not retaining the
dropped welcome (in addition to asserting AccountGroup::visible_for_account(...)
remains empty).

---

Nitpick comments:
In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs`:
- Around line 859-937: The test
test_handle_giftwrap_welcome_blocked_welcomer_with_decoy_rumor_is_dropped
currently mutes the creator/welcomer
(MuteListEntry::insert(&creator_account.pubkey,...)) which walks
welcome.group_admin_pubkeys too; change or add a new regression test that
instead leaves the welcomer allowed and mutes an injected admin key from
welcome.group_admin_pubkeys so the admin-path is exercised independently.
Concretely: create a separate admin Keys (e.g., admin_keys), include
admin_pubkey in the group config via create_nostr_group_config_data so it
appears in welcome.group_admin_pubkeys, call
MuteListEntry::insert(&admin_pubkey, false,
&member_session.account_db).await.unwrap() (not the creator), then build the
same giftwrap via EventBuilder::gift_wrap and call handle_giftwrap; assert the
giftwrap is dropped and AccountGroup::visible_for_account still returns empty.
Ensure the new test name reflects "blocked_admin" to isolate the admin branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99b41010-14c5-46b0-83a7-975007522868

📥 Commits

Reviewing files that changed from the base of the PR and between cd6e78e and 6e2c31f.

📒 Files selected for processing (1)
  • src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs

Comment thread src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)

947-952: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mirror the MDK-state assertion in the blocked-seal-sender regression too.

This closes the gap for the decoy-welcomer path, but test_handle_giftwrap_welcome_blocked_seal_sender_is_dropped still only checks hidden app state. A future reorder that leaves pending MDK membership behind would still pass that test.

Suggested change
         let visible_groups = AccountGroup::visible_for_account(&whitenoise, &member_account.pubkey)
             .await
             .unwrap();
         assert!(
             visible_groups.is_empty(),
             "blocked seal sender must not create a visible account group"
         );
+
+        let mdk_groups = member_session.mdk.get_groups().unwrap();
+        assert!(
+            mdk_groups.is_empty(),
+            "blocked seal sender must not leave pending MDK group state"
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs` around
lines 947 - 952, Add the same MDK-state assertion used for the blocked welcomer
path to the blocked-seal-sender regression: after the code path that handles the
blocked seal sender (the logic exercised by
test_handle_giftwrap_welcome_blocked_seal_sender_is_dropped), call
member_session.mdk.get_groups().unwrap() and assert that the returned groups are
empty (same message text: "blocked welcomer must not leave pending MDK group
state" or adjust to reference blocked-seal-sender), ensuring the test checks
that no pending MDK membership is left behind.
🧹 Nitpick comments (1)
src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs (1)

142-145: ⚡ Quick win

Use this file's module path for the new tracing targets.

These new logs keep the older pseudo-target (whitenoise::event_processor::process_welcome) instead of the module path for src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs, which makes filtering/routing inconsistent with the repo convention.

Suggested change
-            tracing::info!(
-                target: "whitenoise::event_processor::process_welcome",
+            tracing::info!(
+                target: "whitenoise::event_processor::event_handlers::handle_giftwrap",
                 "Dropping welcome from blocked gift-wrap sender {}",
                 seal_sender,
             );
-                    tracing::warn!(
-                        target: "whitenoise::event_processor::process_welcome",
+                    tracing::warn!(
+                        target: "whitenoise::event_processor::event_handlers::handle_giftwrap",
                         error = %e,
                         "Failed to clean up pending MDK group after blocked welcome"
                     );

As per coding guidelines src/whitenoise/**/*.rs: Use target: "whitenoise::module_name" in logging calls to match the module path.

Also applies to: 180-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs` around
lines 142 - 145, The tracing calls in handle_giftwrap.rs are using the old
pseudo-target "whitenoise::event_processor::process_welcome"; update those
tracing::info/debug/error targets to the file's module path (e.g.,
"whitenoise::event_processor::event_handlers::handle_giftwrap" or whatever the
module declared at the top of the file is) so logs follow the repo convention;
locate the tracing invocations in the handle_giftwrap function (the call that
logs "Dropping welcome from blocked gift-wrap sender {}" and the other calls
around lines noted) and replace their target strings accordingly to match the
module path used by this file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs`:
- Around line 947-952: Add the same MDK-state assertion used for the blocked
welcomer path to the blocked-seal-sender regression: after the code path that
handles the blocked seal sender (the logic exercised by
test_handle_giftwrap_welcome_blocked_seal_sender_is_dropped), call
member_session.mdk.get_groups().unwrap() and assert that the returned groups are
empty (same message text: "blocked welcomer must not leave pending MDK group
state" or adjust to reference blocked-seal-sender), ensuring the test checks
that no pending MDK membership is left behind.

---

Nitpick comments:
In `@src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs`:
- Around line 142-145: The tracing calls in handle_giftwrap.rs are using the old
pseudo-target "whitenoise::event_processor::process_welcome"; update those
tracing::info/debug/error targets to the file's module path (e.g.,
"whitenoise::event_processor::event_handlers::handle_giftwrap" or whatever the
module declared at the top of the file is) so logs follow the repo convention;
locate the tracing invocations in the handle_giftwrap function (the call that
logs "Dropping welcome from blocked gift-wrap sender {}" and the other calls
around lines noted) and replace their target strings accordingly to match the
module path used by this file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 698ef378-62d1-4e2a-b665-b9d3bd59e864

📥 Commits

Reviewing files that changed from the base of the PR and between 6e2c31f and d660299.

📒 Files selected for processing (1)
  • src/whitenoise/event_processor/event_handlers/handle_giftwrap.rs

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant