From 84a541b619238427d038e92746102c87a6ac5c36 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:42:03 -0700 Subject: [PATCH] feat: support * wildcard in platform allowlists and improve WhatsApp docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: clarify WhatsApp allowlist behavior and document WHATSAPP_ALLOW_ALL_USERS - Add WHATSAPP_ALLOW_ALL_USERS and WHATSAPP_DEBUG to env vars reference - Warn that * is not a wildcard and silently blocks all messages - Show WHATSAPP_ALLOWED_USERS as optional, not required - Update troubleshooting with the * trap and debug mode tip - Fix Security section to mention the allow-all alternative Prompted by a user report in Discord where WHATSAPP_ALLOWED_USERS=* caused all incoming messages to be silently dropped at the bridge level. * feat: support * wildcard in platform allowlists Follow the precedent set by SIGNAL_GROUP_ALLOWED_USERS which already supports * as an allow-all wildcard. Bridge (allowlist.js): matchesAllowedUser() now checks for * in the allowedUsers set before iterating sender aliases. Gateway (run.py): _is_authorized() checks for * in allowed_ids after parsing the allowlist. This is generic — works for all platforms, not just WhatsApp. Updated docs to document * as a supported value instead of warning against it. Added WHATSAPP_ALLOW_ALL_USERS and WHATSAPP_DEBUG to the env vars reference. Tests: JS allowlist test + 2 Python gateway tests (WhatsApp + Telegram to verify cross-platform behavior). --- gateway/run.py | 5 +++ scripts/whatsapp-bridge/allowlist.js | 5 +++ scripts/whatsapp-bridge/allowlist.test.mjs | 12 ++++++ .../gateway/test_unauthorized_dm_behavior.py | 40 +++++++++++++++++++ .../docs/reference/environment-variables.md | 4 +- website/docs/user-guide/messaging/whatsapp.md | 20 ++++++++-- 6 files changed, 81 insertions(+), 5 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 2fe929447..cc1a6666f 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1650,6 +1650,11 @@ class GatewayRunner: if global_allowlist: allowed_ids.update(uid.strip() for uid in global_allowlist.split(",") if uid.strip()) + # "*" in any allowlist means allow everyone (consistent with + # SIGNAL_GROUP_ALLOWED_USERS precedent) + if "*" in allowed_ids: + return True + check_ids = {user_id} if "@" in user_id: check_ids.add(user_id.split("@")[0]) diff --git a/scripts/whatsapp-bridge/allowlist.js b/scripts/whatsapp-bridge/allowlist.js index 760e413f2..4cbd82d0d 100644 --- a/scripts/whatsapp-bridge/allowlist.js +++ b/scripts/whatsapp-bridge/allowlist.js @@ -68,6 +68,11 @@ export function matchesAllowedUser(senderId, allowedUsers, sessionDir) { return true; } + // "*" means allow everyone (consistent with SIGNAL_GROUP_ALLOWED_USERS) + if (allowedUsers.has('*')) { + return true; + } + const aliases = expandWhatsAppIdentifiers(senderId, sessionDir); for (const alias of aliases) { if (allowedUsers.has(alias)) { diff --git a/scripts/whatsapp-bridge/allowlist.test.mjs b/scripts/whatsapp-bridge/allowlist.test.mjs index 7eea7399c..86e1f1d6b 100644 --- a/scripts/whatsapp-bridge/allowlist.test.mjs +++ b/scripts/whatsapp-bridge/allowlist.test.mjs @@ -45,3 +45,15 @@ test('matchesAllowedUser accepts mapped lid sender when allowlist only contains rmSync(sessionDir, { recursive: true, force: true }); } }); + +test('matchesAllowedUser treats * as allow-all wildcard', () => { + const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-')); + + try { + const allowedUsers = parseAllowedUsers('*'); + assert.equal(matchesAllowedUser('19175395595@s.whatsapp.net', allowedUsers, sessionDir), true); + assert.equal(matchesAllowedUser('267383306489914@lid', allowedUsers, sessionDir), true); + } finally { + rmSync(sessionDir, { recursive: true, force: true }); + } +}); diff --git a/tests/gateway/test_unauthorized_dm_behavior.py b/tests/gateway/test_unauthorized_dm_behavior.py index 25b51dc2f..5f898b5e6 100644 --- a/tests/gateway/test_unauthorized_dm_behavior.py +++ b/tests/gateway/test_unauthorized_dm_behavior.py @@ -90,6 +90,46 @@ def test_whatsapp_lid_user_matches_phone_allowlist_via_session_mapping(monkeypat assert runner._is_user_authorized(source) is True +def test_star_wildcard_in_allowlist_authorizes_any_user(monkeypatch): + """WHATSAPP_ALLOWED_USERS=* should act as allow-all wildcard.""" + _clear_auth_env(monkeypatch) + monkeypatch.setenv("WHATSAPP_ALLOWED_USERS", "*") + + runner, _adapter = _make_runner( + Platform.WHATSAPP, + GatewayConfig(platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}), + ) + + source = SessionSource( + platform=Platform.WHATSAPP, + user_id="99998887776@s.whatsapp.net", + chat_id="99998887776@s.whatsapp.net", + user_name="stranger", + chat_type="dm", + ) + assert runner._is_user_authorized(source) is True + + +def test_star_wildcard_works_for_any_platform(monkeypatch): + """The * wildcard should work generically, not just for WhatsApp.""" + _clear_auth_env(monkeypatch) + monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "*") + + runner, _adapter = _make_runner( + Platform.TELEGRAM, + GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), + ) + + source = SessionSource( + platform=Platform.TELEGRAM, + user_id="123456789", + chat_id="123456789", + user_name="stranger", + chat_type="dm", + ) + assert runner._is_user_authorized(source) is True + + @pytest.mark.asyncio async def test_unauthorized_dm_pairs_by_default(monkeypatch): _clear_auth_env(monkeypatch) diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index fd57ffb02..10b6367be 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -170,7 +170,9 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe | `SLACK_HOME_CHANNEL_NAME` | Display name for the Slack home channel | | `WHATSAPP_ENABLED` | Enable the WhatsApp bridge (`true`/`false`) | | `WHATSAPP_MODE` | `bot` (separate number) or `self-chat` (message yourself) | -| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code, no `+`) | +| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code, no `+`), or `*` to allow all senders | +| `WHATSAPP_ALLOW_ALL_USERS` | Allow all WhatsApp senders without an allowlist (`true`/`false`) | +| `WHATSAPP_DEBUG` | Log raw message events in the bridge for troubleshooting (`true`/`false`) | | `SIGNAL_HTTP_URL` | signal-cli daemon HTTP endpoint (for example `http://127.0.0.1:8080`) | | `SIGNAL_ACCOUNT` | Bot phone number in E.164 format | | `SIGNAL_ALLOWED_USERS` | Comma-separated E.164 phone numbers or UUIDs | diff --git a/website/docs/user-guide/messaging/whatsapp.md b/website/docs/user-guide/messaging/whatsapp.md index 1c5226813..6011992ec 100644 --- a/website/docs/user-guide/messaging/whatsapp.md +++ b/website/docs/user-guide/messaging/whatsapp.md @@ -94,9 +94,20 @@ Add the following to your `~/.hermes/.env` file: # Required WHATSAPP_ENABLED=true WHATSAPP_MODE=bot # "bot" or "self-chat" + +# Access control — pick ONE of these options: WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers (with country code, no +) +# WHATSAPP_ALLOWED_USERS=* # OR use * to allow everyone +# WHATSAPP_ALLOW_ALL_USERS=true # OR set this flag instead (same effect as *) ``` +:::tip Allow-all shorthand +Setting `WHATSAPP_ALLOWED_USERS=*` allows **all** senders (equivalent to `WHATSAPP_ALLOW_ALL_USERS=true`). +This is consistent with [Signal group allowlists](/docs/reference/environment-variables). +To use the pairing flow instead, remove both variables and rely on the +[DM pairing system](/docs/user-guide/security#dm-pairing-system). +::: + Optional behavior settings in `~/.hermes/config.yaml`: ```yaml @@ -174,7 +185,7 @@ whatsapp: | **Bridge crashes or reconnect loops** | Restart the gateway, update Hermes, and re-pair if the session was invalidated by a WhatsApp protocol change. | | **Bot stops working after WhatsApp update** | Update Hermes to get the latest bridge version, then re-pair. | | **macOS: "Node.js not installed" but node works in terminal** | launchd services don't inherit your shell PATH. Run `hermes gateway install` to re-snapshot your current PATH into the plist, then `hermes gateway start`. See the [Gateway Service docs](./index.md#macos-launchd) for details. | -| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces). | +| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces), or set it to `*` to allow everyone. Set `WHATSAPP_DEBUG=true` in `.env` and restart the gateway to see raw message events in `bridge.log`. | | **Bot replies to strangers with a pairing code** | Set `whatsapp.unauthorized_dm_behavior: ignore` in `~/.hermes/config.yaml` if you want unauthorized DMs to be silently ignored instead. | --- @@ -182,9 +193,10 @@ whatsapp: ## Security :::warning -**Always set `WHATSAPP_ALLOWED_USERS`** with phone numbers (including country code, without the `+`) -of authorized users. Without this setting, the gateway will **deny all incoming messages** as a -safety measure. +**Configure access control** before going live. Set `WHATSAPP_ALLOWED_USERS` with specific +phone numbers (including country code, without the `+`), use `*` to allow everyone, or set +`WHATSAPP_ALLOW_ALL_USERS=true`. Without any of these, the gateway **denies all incoming +messages** as a safety measure. ::: By default, unauthorized DMs still receive a pairing code reply. If you want a private WhatsApp number to stay completely silent to strangers, set: