WhatsApp allowlist and self-chat authorization controls

NousResearch/hermes-agent · cluster 8389 · gateway

Data as of

Nothing open on GitHub. Fix merged in #21291. Check it covers your case.

The short answer

0open issues
0open PRs
7items grouped
Known?
Yes. 7 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #21291. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #21291. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests address authorization behavior in WhatsApp integration, focusing on how an empty or missing WHATSAPP_ALLOWED_USERS allowlist is handled and how self-chat mode responds to incoming messages from unknown senders.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #21291 fix(whatsapp): reject strangers by default, never respond in self-chat (#8389)

    merged to main

    files: scripts/whatsapp-bridge/allowlist.js, scripts/whatsapp-bridge/allowlist.test.mjs, scripts/whatsapp-bridge/bridge.js

    Show the diff
    • scripts/whatsapp-bridge/allowlist.js
      @@ -64,8 +64,12 @@ export function expandWhatsAppIdentifiers(identifier, sessionDir) {
       }
       
       export function matchesAllowedUser(senderId, allowedUsers, sessionDir) {
      +  // Empty allowlist = NO ONE allowed (secure default, #8389).  Operators
      +  // who want an open bot must set ``WHATSAPP_ALLOWED_USERS=*`` explicitly.
      +  // Previous behaviour (empty → return true) let any stranger DM the
      +  // bridge and trigger a Python-side pairing-code reply.
         if (!allowedUsers || allowedUsers.size === 0) {
      -    return true;
      +    return false;
         }
       
         // "*" means allow everyone (consistent with SIGNAL_GROUP_ALLOWED_USERS)
    • scripts/whatsapp-bridge/allowlist.test.mjs
      @@ -57,3 +57,24 @@ test('matchesAllowedUser treats * as allow-all wildcard', () => {
           rmSync(sessionDir, { recursive: true, force: true });
         }
       });
      +
      +test('matchesAllowedUser rejects everyone when allowlist is empty (#8389)', () => {
      +  // Regression guard: empty allowlist used to return true (allow-everyone),
      +  // which let any stranger DM the bridge and trigger a Python-side
      +  // pairing-code reply. Secure default is now "reject unless explicitly
      +  // configured"; operators who want an open bot must set `*`.
      +  const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-'));
      +
      +  try {
      +    const empty = parseAllowedUsers('');
      +    assert.equal(empty.size, 0);
      +    assert.equal(matchesAllowedUser('[redacted:email]', empty, sessionDir), false);
      +    assert.equal(matchesAllowedUser('267383306489914@lid', empty, sessionDir), false);
      +
      +    // Null/undefined allowlist (defensive) also rejects.
      +    assert.equal(matchesAllowedUser('[redacted:email]', null, sessionDir), false);
      +    assert.equal(matchesAllowedUser('[redacted:email]', undefined, sessionDir), false);
      +  } finally {
      +    rmSync(sessionDir, { recursive: true, force: true });
      +  }
      +});
    • scripts/whatsapp-bridge/bridge.js
      @@ -229,17 +229,34 @@ async function startSocket() {
               if (!isSelfChat) continue;
             }
       
      -      // Check allowlist for messages from others (resolve LID ↔ phone aliases)
      -      if (!msg.key.fromMe && !matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
      -        try {
      -          console.log(JSON.stringify({
      -            event: 'ignored',
      -            reason: 'allowlist_mismatch',
      -            chatId,
      -            senderId,
      -          }));
      -        } catch {}
      -        continue;
      +      // Handle !fromMe messages (from other people) based on mode.
      +      // Self-chat mode only responds to the user's own messages to
      +      // themselves — stranger DMs / group pings must never reach the
      +      // Python gateway, otherwise a pairing-code reply fires in response
      +      // to arbitrary incoming messages (#8389).
      +      if (!msg.key.fromMe) {
      +        if (WHATSAPP_MODE === 'self-chat') {
      +          try {
      +            console.log(JSON.stringify({
      +              event: 'ignored',
      +              reason: 'self_chat_mode_rejects_non_self',
      +              chatId,
      +              senderId,
      +            }));
      +          } catch {}
      +          continue;
      +        }
      +        if (!matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
      +          try {
      +            console.log(JSON.stringify({
      +              event: 'ignored',
      +              reason: 'allowlist_mismatch',
      +              chatId,
      +              senderId,
      +            }));
      +          } catch {}
      +          continue;
      +        }
             }
       
             const messageContent = getMessageContent(msg);
      @@ -625,8 +642,12 @@ if (PAIR_ONLY) {
           console.log(`📁 Session stored in: ${SESSION_DIR}`);
           if (ALLOWED_USERS.size > 0) {
             console.log(`🔒 Allowed users: ${Array.from(ALLOWED_USERS).join(', ')}`);
      +    } else if (WHATSAPP_MODE === 'self-chat') {
      +      console.log(`🔒 Self-chat mode — only your own messages to yourself are processed.`);
           } else {
      -      console.log(`⚠️  No WHATSAPP_ALLOWED_USERS set — all messages will be processed`);
      +      console.log(`🔒 No WHATSAPP_ALLOWED_USERS set — incoming messages are rejected.`);
      +      console.log(`   Set WHATSAPP_ALLOWED_USERS=<phone> to authorize specific users,`);
      +      console.log(`   or WHATSAPP_ALLOWED_USERS=* for an explicit open bot.`);
           }
           console.log();
           startSocket();

4 PRs closed without merging

From the close comments: done elsewhere 2 · superseded by another PR 2

  • #8410 security(whatsapp): deny unknown senders when WHATSAPP_ALLOWED_USERS is empty (#8389) · superseded by another PR → #21291, #8389, #8431
  • #8431 fix(whatsapp/bridge): #8389 patch critical authorization bypass in self-chat mode · done elsewhere → #8389
  • #21009 fix(whatsapp): deny empty bridge allowlist · done elsewhere → 413990c
  • #30288 fix(whatsapp): allow group policy gating in self-chat mode · superseded by another PR → #20429
Closed issues · 2
  • Closed#8389`[SECURITY] WhatsApp self-chat mode responds to ALL incoming messages (empty allowlist = allow everyone)`
  • Closed#15108WhatsApp setup: WHATSAPP_ALLOWED_USERS should be mandatory
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.26: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#21291
Closed issues
2
Attempts
4 PRs closed without merging
Fix link checked
7 May 2026

For your agent

The same facts, typed.