Skip to content

fix(authz): normalize phone-number handles for the BlueBubbles allowlist - #82549

Open
withzombies wants to merge 1 commit into
NousResearch:mainfrom
withzombies:fix/bluebubbles-authz-phone-normalization
Open

fix(authz): normalize phone-number handles for the BlueBubbles allowlist#82549
withzombies wants to merge 1 commit into
NousResearch:mainfrom
withzombies:fix/bluebubbles-authz-phone-normalization

Conversation

@withzombies

Copy link
Copy Markdown

What does this PR do?

BLUEBUBBLES_ALLOWED_USERS never matched phone-number senders unless the operator's entry was byte-for-byte identical to the wire form of the handle.

BlueBubbles (iMessage) handles are phone numbers or Apple ID email addresses. _is_user_authorized (gateway/authz_mixin.py) compared allowlist entries against the inbound user_id with raw string equality — the check_ids block has per-platform normalization branches for WhatsApp (phone↔LID/JID aliases) and SimpleX (display-name alias), but nothing for BlueBubbles. An operator naturally writes +1 (555) 123-0001 while the wire delivers +15551230001 (or they paste the digits without the +), so the allowlist silently denies every listed sender. The observable failure mode is operators "fixing" it with BLUEBUBBLES_ALLOW_ALL_USERS=true — the exact opposite of SECURITY.md §2.6, rule 2: "An allowlist is required for every enabled network-exposed adapter. … Code paths that fail open when no allowlist is configured are code bugs." An allowlist that can't match its own operator's entries pushes deployments into that fail-open posture.

The fix adds a Platform.BLUEBUBBLES branch to the check_ids block, structurally mirroring the WhatsApp branch:

  • New module-level _normalize_bluebubbles_handle(value) (placed with the other normalization helpers): strip/lowercase; entries containing @ are treated as emails and kept as-is (lowercased); everything else is reduced to digits only; empty input yields "".
  • Allowlist entries gain their normalized forms as a union with the originals, so any exact-match setup that works today keeps working unchanged.
  • The normalized inbound user_id is added to check_ids.
  • Empty normalized forms are never added on either side, so a digit-free entry can't cross-match a digit-free sender via "" == "".

The * wildcard, BLUEBUBBLES_ALLOW_ALL_USERS, and the empty-allowlist default-deny paths are untouched (they resolve before this branch), and no other platform's comparison changes — the branch is gated on source.platform == Platform.BLUEBUBBLES.

Related Issue

No existing issue found (searches below). Happy to open one first if preferred.

Fixes # (none — see duplicate search)

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

(Arguably security-adjacent: it removes the incentive to set BLUEBUBBLES_ALLOW_ALL_USERS, but the change itself is a matching bug fix.)

Changes Made

  • gateway/authz_mixin.py
    • New module-level _normalize_bluebubbles_handle() next to the imported WhatsApp normalizers: emails lowercased as-is, phone numbers reduced to digits only, "" for empty input.
    • New Platform.BLUEBUBBLES branch in _is_user_authorized's check_ids block (between the WhatsApp and SimpleX branches): unions normalized allowlist entries into allowed_ids and adds the normalized user_id to check_ids, both guarded against empty normalized forms.
  • tests/gateway/test_bluebubbles_authz.py (new)
    • 15 tests covering: formatted-entry↔wire-form matching both directions, digits-only entry vs +-prefixed wire form, GATEWAY_ALLOWED_USERS entries for BlueBubbles, case-insensitive email matching both directions, unlisted phone/email still denied, no-allowlist default-deny, digit-free entries not cross-matching via "", * wildcard, BLUEBUBBLES_ALLOW_ALL_USERS, and non-regression on other platforms (a formatted Telegram entry does NOT digit-match a numeric id; exact Telegram ids still match).

How to Test

  1. pytest tests/gateway/test_bluebubbles_authz.py -q → 15 passed.
  2. pytest tests/gateway/ -q -k "auth" → 234 passed, 4 skipped, 1 xfailed (no regressions in the existing authorization suite).
  3. Manual repro of the bug on main: set BLUEBUBBLES_ALLOWED_USERS="+1 (555) 123-0001", send an iMessage from that number (wire handle +15551230001) → gateway logs an unauthorized-user denial. On this branch the same setup authorizes; an unlisted number is still denied, and unsetting the allowlist still default-denies.
  4. Quick unit check of the normalizer:
    python -c "from gateway.authz_mixin import _normalize_bluebubbles_handle as n; print(n('+1 (555) 123-0001'), n('Contact@Example.COM'), repr(n(' ')))"15551230001 contact@example.com ''.

Duplicate search

Run 2026-08-09 with gh search prs/issues --repo NousResearch/hermes-agent (open + closed):

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass (new file + -k "auth" slice run directly; see How to Test)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26 (Darwin 25.3.0)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — docstrings on the new helper and branch; no user-facing env-var semantics doc changes needed (the var behaves as documented, it just matches now)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — pure-string logic, platform-independent
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

$ pytest tests/gateway/test_bluebubbles_authz.py -q
...............                                                          [100%]
15 passed in 1.00s

$ pytest tests/gateway/ -q -k "auth"
234 passed, 4 skipped, 4879 deselected, 1 xfailed, 8 warnings in 16.13s

$ uvx ruff@0.15.10 check gateway/ tests/
All checks passed!

🤖 Generated with Claude Code

BlueBubbles (iMessage) handles are phone numbers or Apple ID email
addresses, but _is_user_authorized compared BLUEBUBBLES_ALLOWED_USERS
entries against the inbound user_id with raw string equality. An
operator naturally writes "+1 (555) 123-0001" while the wire delivers
"+15551230001" (or vice versa), so the allowlist silently never matched
and every allowlisted sender was denied — pushing operators toward
BLUEBUBBLES_ALLOW_ALL_USERS, the opposite of SECURITY.md §2.6's rule
that every network-exposed adapter be gated by an allowlist.

Add a Platform.BLUEBUBBLES branch to the check_ids block, mirroring the
WhatsApp alias-normalization branch: a module-level
_normalize_bluebubbles_handle() lowercases emails as-is and reduces
phone numbers to digits only, applied to both the allowlist entries
(as a union, so exact-match setups keep working) and the inbound
user_id. Empty normalized forms are never added, so a digit-free entry
cannot cross-match. The "*" wildcard, ALLOW_ALL flag, and
empty-allowlist default-deny paths are untouched, and no other
platform's comparison changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants