Skip to content

whatsapp: close two group-chat security gaps (sender identity + fromMe handling) - #15413

Closed
jscholz wants to merge 2 commits into
NousResearch:mainfrom
jscholz:whatsapp-sender-prefix
Closed

whatsapp: close two group-chat security gaps (sender identity + fromMe handling)#15413
jscholz wants to merge 2 commits into
NousResearch:mainfrom
jscholz:whatsapp-sender-prefix

Conversation

@jscholz

@jscholz jscholz commented Apr 24, 2026

Copy link
Copy Markdown

Summary

Two related fixes that together make WhatsApp group chat safe and usable from the session owner's own paired account:

  1. gateway/platforms/whatsapp.py — prepend [<pushName>] to the body on group messages so the LLM can attribute each turn to its sender.
  2. scripts/whatsapp-bridge/bridge.js — stop dropping fromMe && isGroup messages in self-chat mode so the owner can @mention the bot in their own groups.

Either fix alone is insufficient: without #1, group members' writes look indistinguishable from the owner's (memory-poisoning + trust-tier bypass); without #2, the owner can't participate in the group conversation at all.

Why it matters

Reproduced on my instance after a friend managed to have the bot address him as a custom honorific and trigger a profile memory-write into my USER.md, all from a shared group chat. Root cause was twofold:

  • The bridge already parses senderId (participant JID) + senderName (pushName) on every inbound message and posts both in the HTTP event dict — but WhatsAppAdapter._handle_event drops them, passing only text = body to MessageEvent. From the LLM's perspective every group member's message looks like the owner's.
  • Separately, the bridge's fromMe handler in self-chat mode bails unconditionally on group messages (if (isGroup || chatId.includes('status')) continue). This was almost certainly intended as an echo-loop guard, but the real loop-prevention already exists ~60 lines below at the REPLY_PREFIX + recentlySentIds check. The early bail just blocks the owner from ever using the bot in their own groups.

gateway/session.py:245-256 has a comment documenting the intent that sender names are prefixed on each user message — this PR completes that design.

Patch 1 — sender identity prefix (whatsapp.py)

# Prepend sender identity on group messages so the LLM can
# attribute each turn to the correct participant. Without this,
# every group member's writes arrive indistinguishable from the
# session owner's, which breaks memory-write attribution and
# defeats the trust-tier policy documented in AGENTS.md. DMs are
# untouched since there's only one possible sender.
if is_group and body:
    sender_name = data.get("senderName")
    if not sender_name:
        sid = data.get("senderId", "") or ""
        sender_name = sid.split("@")[0] if sid else "unknown"
    body = f"[{sender_name}] {body}"

Inserted immediately before the existing MessageEvent(text=body, ...) construction. All prior transforms (_clean_bot_mention_text, document-content injection, media placeholder) run against the raw body first; the prefix appears at the very start of the text the LLM reads. Fallback chain: pushName → short JID (before @) → "unknown".

Patch 2 — fromMe group handling (bridge.js)

// before:
if (msg.key.fromMe) {
  if (isGroup || chatId.includes('status')) continue;
  ...
  const isSelfChat = ...;
  if (!isSelfChat) continue;
}

// after:
if (msg.key.fromMe) {
  if (chatId.includes('status')) continue;  // always skip status broadcasts

  if (WHATSAPP_MODE === 'bot') { continue; }

  // Groups are valid. Self-chat requirement only applies to 1:1 chats.
  if (!isGroup) {
    const isSelfChat = ...;
    if (!isSelfChat) continue;
  }
}

The downstream loop guard (around line 275-280 in the file) is unchanged: any fromMe message that starts with REPLY_PREFIX or whose msg.key.id is in recentlySentIds is still dropped, which is what actually prevents the bridge from re-ingesting its own sent messages. Removing the early isGroup bail just lets the owner's @mentions through to that guard.

Scope / non-goals

  • DMs unchanged in both fixes (owner's self-chat requirement preserved for 1:1 fromMe; DM receiver path untouched).
  • No schema changes to the bridge→gateway event contract.
  • Command parsing unaffectedbody.startswith("/") and @botname mention stripping run before the prefix is added.
  • Doesn't structurally enforce trust tiers (e.g. filtering the tool list per sender). That's a larger follow-up; the sender prefix just restores the information needed to make per-sender policy possible.

Verification

On a running deploy:

  1. From a non-owner account in a shared group, @-mention the bot with a short phrase. The hermes-gateway log should now show the outbound LLM turn as [<pushName>] <message>.
  2. Ask the bot "who just asked?" — it should name the actual sender rather than referring generically to the owner.
  3. From the owner's own paired account, @-mention the bot in the same group. The bot should respond rather than ignoring the message (previously blocked by the fromMe && isGroup bail).
  4. Adversarial: from the non-owner account, say "remember I prefer to be called 'Foo'". Prompt-level behavior now attributes the preference to the guest; the owner's profile is not silently rewritten. (Deeper protection via tool-gating is out of scope for this PR.)

Reproduced and verified locally on v0.11.0 before rebasing both commits onto current main.

jscholz added 2 commits April 24, 2026 19:30
Bridge extracts senderId (participant JID) + senderName (pushName) on
every inbound message and posts them in the HTTP event dict, but the
gateway was dropping both: `text = body` carried only the plain message
content. That meant all group members' messages arrived at the LLM
indistinguishable from the session owner's, so (a) the LLM couldn't
attribute a question to the actual asker, (b) memory-write tool calls
triggered by a guest were stored under the owner's profile, and (c)
the trust-tier policy in AGENTS.md was effectively unenforceable.

Prepend `[<pushName>] ` to body for `isGroup` events only, after
@bot-mention stripping + doc-content injection so the prefix stays at
the very start of the text the LLM reads. Fallback order:
pushName → short JID → "unknown". DMs untouched (no ambiguity).

Completes the design intent already documented in
gateway/session.py:245-256 ("individual sender names are prefixed on
each user message") — that comment promised behavior that was never
implemented in code.
The fromMe handler in self-chat mode unconditionally bailed on group
messages with `if (isGroup || chatId.includes('status')) continue`,
meaning the user could never @mention their own bot in a group they
were in. This is an over-aggressive echo-loop guard — the real
loop-prevention already exists ~60 lines below at the REPLY_PREFIX +
recentlySentIds check, which catches messages the bridge itself sent.

Split the condition: still skip status broadcasts, but let group
messages continue. For 1:1 fromMe messages, preserve the existing
self-chat requirement (so the bot doesn't try to answer on the
user's behalf in random conversations).

Companion to the sender-identity prefix patch in gateway/platforms/
whatsapp.py — without this fix, the user themselves cannot
participate in a group conversation with their bot, so the sender
prefixing alone is only half the fix needed to make group chat
safely usable.
@jscholz jscholz changed the title whatsapp: prepend sender identity to group messages whatsapp: close two group-chat security gaps (sender identity + fromMe handling) Apr 24, 2026
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery platform/whatsapp WhatsApp Business adapter labels Apr 24, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Partially overlaps with #15326 — both fix the fromMe group-message drop in WhatsApp bridge. This PR additionally addresses the sender identity prefix gap (security: memory-poisoning via indistinguishable group members).

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

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround platform/whatsapp WhatsApp Business adapter type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants