{"repo":"NousResearch/hermes-agent","public_id":"8389","display_label":"WhatsApp allowlist and self-chat authorization controls","summary":"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.","subsystem":"gateway","counts":{"members":7,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":21291},"resolution":{"state":"partially_resolved","by":"pr-21291"},"freshness":{"state":"verified","last_verified_at":"2026-05-07T13:53:04+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.2552380952380952},"fix_edges":[{"issue_number":8389,"pr_number":21291,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":8389,"title":"`[SECURITY] WhatsApp self-chat mode responds to ALL incoming messages (empty allowlist = allow everyone)`","url":null,"state":"closed"},{"kind":"issue","number":15108,"title":"WhatsApp setup: WHATSAPP_ALLOWED_USERS should be mandatory","url":null,"state":"closed"},{"kind":"pr","number":8410,"title":"security(whatsapp): deny unknown senders when WHATSAPP_ALLOWED_USERS is empty (#8389)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21291","number":21291},{"kind":"pr","value":"#8389","number":8389},{"kind":"pr","value":"#21291","number":21291},{"kind":"pr","value":"#8431","number":8431}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":8431,"title":"fix(whatsapp/bridge):  #8389 patch critical authorization bypass in self-chat mode ","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#8389","number":8389}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":21009,"title":"fix(whatsapp): deny empty bridge allowlist","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"413990c94537e9c9da973bb21a6afcd332400b91","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":21291,"title":"fix(whatsapp): reject strangers by default, never respond in self-chat (#8389)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/whatsapp-bridge/allowlist.js","scripts/whatsapp-bridge/allowlist.test.mjs","scripts/whatsapp-bridge/bridge.js"],"fix_hunks":[{"file":"scripts/whatsapp-bridge/allowlist.js","hunks":["@@ -64,8 +64,12 @@ export function expandWhatsAppIdentifiers(identifier, sessionDir) {\n }\n \n export function matchesAllowedUser(senderId, allowedUsers, sessionDir) {\n+  // Empty allowlist = NO ONE allowed (secure default, #8389).  Operators\n+  // who want an open bot must set ``WHATSAPP_ALLOWED_USERS=*`` explicitly.\n+  // Previous behaviour (empty → return true) let any stranger DM the\n+  // bridge and trigger a Python-side pairing-code reply.\n   if (!allowedUsers || allowedUsers.size === 0) {\n-    return true;\n+    return false;\n   }\n \n   // \"*\" means allow everyone (consistent with SIGNAL_GROUP_ALLOWED_USERS)"]},{"file":"scripts/whatsapp-bridge/allowlist.test.mjs","hunks":["@@ -57,3 +57,24 @@ test('matchesAllowedUser treats * as allow-all wildcard', () => {\n     rmSync(sessionDir, { recursive: true, force: true });\n   }\n });\n+\n+test('matchesAllowedUser rejects everyone when allowlist is empty (#8389)', () => {\n+  // Regression guard: empty allowlist used to return true (allow-everyone),\n+  // which let any stranger DM the bridge and trigger a Python-side\n+  // pairing-code reply. Secure default is now \"reject unless explicitly\n+  // configured\"; operators who want an open bot must set `*`.\n+  const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-'));\n+\n+  try {\n+    const empty = parseAllowedUsers('');\n+    assert.equal(empty.size, 0);\n+    assert.equal(matchesAllowedUser('[redacted:email]', empty, sessionDir), false);\n+    assert.equal(matchesAllowedUser('267383306489914@lid', empty, sessionDir), false);\n+\n+    // Null/undefined allowlist (defensive) also rejects.\n+    assert.equal(matchesAllowedUser('[redacted:email]', null, sessionDir), false);\n+    assert.equal(matchesAllowedUser('[redacted:email]', undefined, sessionDir), false);\n+  } finally {\n+    rmSync(sessionDir, { recursive: true, force: true });\n+  }\n+});"]},{"file":"scripts/whatsapp-bridge/bridge.js","hunks":["@@ -229,17 +229,34 @@ async function startSocket() {\n         if (!isSelfChat) continue;\n       }\n \n-      // Check allowlist for messages from others (resolve LID ↔ phone aliases)\n-      if (!msg.key.fromMe && !matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {\n-        try {\n-          console.log(JSON.stringify({\n-            event: 'ignored',\n-            reason: 'allowlist_mismatch',\n-            chatId,\n-            senderId,\n-          }));\n-        } catch {}\n-        continue;\n+      // Handle !fromMe messages (from other people) based on mode.\n+      // Self-chat mode only responds to the user's own messages to\n+      // themselves — stranger DMs / group pings must never reach the\n+      // Python gateway, otherwise a pairing-code reply fires in response\n+      // to arbitrary incoming messages (#8389).\n+      if (!msg.key.fromMe) {\n+        if (WHATSAPP_MODE === 'self-chat') {\n+          try {\n+            console.log(JSON.stringify({\n+              event: 'ignored',\n+              reason: 'self_chat_mode_rejects_non_self',\n+              chatId,\n+              senderId,\n+            }));\n+          } catch {}\n+          continue;\n+        }\n+        if (!matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {\n+          try {\n+            console.log(JSON.stringify({\n+              event: 'ignored',\n+              reason: 'allowlist_mismatch',\n+              chatId,\n+              senderId,\n+            }));\n+          } catch {}\n+          continue;\n+        }\n       }\n \n       const messageContent = getMessageContent(msg);","@@ -625,8 +642,12 @@ if (PAIR_ONLY) {\n     console.log(`📁 Session stored in: ${SESSION_DIR}`);\n     if (ALLOWED_USERS.size > 0) {\n       console.log(`🔒 Allowed users: ${Array.from(ALLOWED_USERS).join(', ')}`);\n+    } else if (WHATSAPP_MODE === 'self-chat') {\n+      console.log(`🔒 Self-chat mode — only your own messages to yourself are processed.`);\n     } else {\n-      console.log(`⚠️  No WHATSAPP_ALLOWED_USERS set — all messages will be processed`);\n+      console.log(`🔒 No WHATSAPP_ALLOWED_USERS set — incoming messages are rejected.`);\n+      console.log(`   Set WHATSAPP_ALLOWED_USERS=<phone> to authorize specific users,`);\n+      console.log(`   or WHATSAPP_ALLOWED_USERS=* for an explicit open bot.`);\n     }\n     console.log();\n     startSocket();\n"]}]},{"kind":"pr","number":30288,"title":"fix(whatsapp): allow group policy gating in self-chat mode","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20429","number":20429}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true}],"trust":{"default_tier":"verified","includes_suggested":false}}