mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
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=*.
This commit is contained in:
parent
76d2dcdc8e
commit
6a4ecc0a9f
3 changed files with 59 additions and 13 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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=<phone> to authorize specific users,`);
|
||||
console.log(` or WHATSAPP_ALLOWED_USERS=* for an explicit open bot.`);
|
||||
}
|
||||
console.log();
|
||||
startSocket();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue