From 6a4ecc0a9fdb857cd6ef93cf0ebce77250a2a290 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 7 May 2026 06:53:04 -0700 Subject: [PATCH] fix(whatsapp): reject strangers by default, never respond in self-chat (#8389) (#21291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-chat mode (default) previously replied to ANY incoming DM with a Python-side pairing-code message. Two compounding defaults: 1. allowlist.js::matchesAllowedUser returned true for an empty allowlist — so WHATSAPP_ALLOWED_USERS unset → everyone passes the JS bridge gate → messages reach Python gateway → _is_user_authorized returns False but _get_unauthorized_dm_behavior falls back to 'pair' → stranger gets a pairing code reply. 2. bridge.js had no mode check on !fromMe messages, so self-chat mode (where the operator only wants to talk to themselves) forwarded everything anyway. Fix: - allowlist.js: empty allowlist now returns false. Operators who want an open bot must set WHATSAPP_ALLOWED_USERS=* explicitly (the existing wildcard behaviour, consistent with SIGNAL_GROUP_ALLOWED_USERS). - bridge.js: self-chat mode hard-rejects all !fromMe messages at the bridge, before they ever reach the Python gateway. Bot mode still enforces the allowlist. - Startup log message updated to reflect the new per-mode behaviour (was '⚠️ No WHATSAPP_ALLOWED_USERS set — all messages will be processed', which was both inaccurate post-fix and a bad default signal pre-fix). - allowlist.test.mjs: new regression test pinning the empty-rejects contract, + null/undefined defensive cases. Behaviour delta for existing users: - self-chat mode, no allowlist: strangers got pairing codes, now silently dropped. Strictly better. - bot mode, no allowlist: strangers got pairing codes via the Python-side pairing flow, now silently dropped at the JS bridge. Operators who genuinely want an open bot set WHATSAPP_ALLOWED_USERS=*. --- scripts/whatsapp-bridge/allowlist.js | 6 ++- scripts/whatsapp-bridge/allowlist.test.mjs | 21 ++++++++++ scripts/whatsapp-bridge/bridge.js | 45 ++++++++++++++++------ 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/scripts/whatsapp-bridge/allowlist.js b/scripts/whatsapp-bridge/allowlist.js index 4cbd82d0d2..ffc8949a7b 100644 --- a/scripts/whatsapp-bridge/allowlist.js +++ b/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) diff --git a/scripts/whatsapp-bridge/allowlist.test.mjs b/scripts/whatsapp-bridge/allowlist.test.mjs index 86e1f1d6bd..c6ca1cb3c4 100644 --- a/scripts/whatsapp-bridge/allowlist.test.mjs +++ b/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('19175395595@s.whatsapp.net', empty, sessionDir), false); + assert.equal(matchesAllowedUser('267383306489914@lid', empty, sessionDir), false); + + // Null/undefined allowlist (defensive) also rejects. + assert.equal(matchesAllowedUser('19175395595@s.whatsapp.net', null, sessionDir), false); + assert.equal(matchesAllowedUser('19175395595@s.whatsapp.net', undefined, sessionDir), false); + } finally { + rmSync(sessionDir, { recursive: true, force: true }); + } +}); diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index 162acdaca1..9ab6118da1 100644 --- a/scripts/whatsapp-bridge/bridge.js +++ b/scripts/whatsapp-bridge/bridge.js @@ -267,17 +267,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); @@ -676,8 +693,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= to authorize specific users,`); + console.log(` or WHATSAPP_ALLOWED_USERS=* for an explicit open bot.`); } console.log(); startSocket();