mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-21 17:12:32 +00:00
fix(signal): normalize identifiers when matching DM allowlist
The DM allowlist check split sender_id on '|' and looked for raw membership in the allow_from list. Senders carry their phone number with a leading '+' but admins routinely write allowlist entries without it (or vice versa), and UUID/ACI matches were case-sensitive. Both forms now flow through _normalize_signal_id, so an entry like 19995550001 matches a sender +19995550001 and a UUID matches case-insensitively. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d56bafa6d0
commit
1377759705
@ -695,10 +695,7 @@ class SignalChannel(BaseChannel):
|
|||||||
self.logger.debug(f"Ignoring DM from {sender_id} (DMs disabled)")
|
self.logger.debug(f"Ignoring DM from {sender_id} (DMs disabled)")
|
||||||
return
|
return
|
||||||
if self.config.dm.policy == "allowlist":
|
if self.config.dm.policy == "allowlist":
|
||||||
allow_list = self.config.dm.allow_from
|
if not self._sender_matches_allowlist(sender_id, self.config.dm.allow_from):
|
||||||
sender_str = str(sender_id)
|
|
||||||
parts = [sender_str] + (sender_str.split("|") if "|" in sender_str else [])
|
|
||||||
if not any(p for p in parts if p in allow_list):
|
|
||||||
self.logger.debug(f"Ignoring DM from {sender_id} (policy: {self.config.dm.policy})")
|
self.logger.debug(f"Ignoring DM from {sender_id} (policy: {self.config.dm.policy})")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -864,6 +861,28 @@ class SignalChannel(BaseChannel):
|
|||||||
normalized.append(f"+{raw}")
|
normalized.append(f"+{raw}")
|
||||||
return list(dict.fromkeys(normalized))
|
return list(dict.fromkeys(normalized))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _sender_matches_allowlist(cls, sender_id: str, allow_list: list[str]) -> bool:
|
||||||
|
"""Return True if any normalized variant of sender_id is on allow_list.
|
||||||
|
|
||||||
|
sender_id is the pipe-joined identifier string built by
|
||||||
|
_collect_sender_id_parts. Each part and each allow_list entry is run
|
||||||
|
through _normalize_signal_id so an allowlist entry like ``1234567890``
|
||||||
|
matches a sender ``+1234567890`` (and vice versa), and case-only
|
||||||
|
differences in UUIDs/ACIs match too.
|
||||||
|
"""
|
||||||
|
if not allow_list:
|
||||||
|
return False
|
||||||
|
sender_variants: set[str] = set()
|
||||||
|
for part in str(sender_id).split("|"):
|
||||||
|
sender_variants.update(cls._normalize_signal_id(part))
|
||||||
|
if not sender_variants:
|
||||||
|
return False
|
||||||
|
allow_variants: set[str] = set()
|
||||||
|
for entry in allow_list:
|
||||||
|
allow_variants.update(cls._normalize_signal_id(entry))
|
||||||
|
return bool(sender_variants & allow_variants)
|
||||||
|
|
||||||
def _remember_account_id_alias(self, value: str | None) -> None:
|
def _remember_account_id_alias(self, value: str | None) -> None:
|
||||||
"""Remember known bot identifiers for mention matching."""
|
"""Remember known bot identifiers for mention matching."""
|
||||||
if not value:
|
if not value:
|
||||||
|
|||||||
@ -510,6 +510,36 @@ class TestHandleDataMessageDM:
|
|||||||
await ch._handle_receive_notification(params)
|
await ch._handle_receive_notification(params)
|
||||||
assert handled == []
|
assert handled == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dm_allowlist_matches_without_plus_prefix(self):
|
||||||
|
"""An allowlist entry without '+' must match a sender that carries '+'."""
|
||||||
|
ch, handled = self._make_dm_channel(policy="allowlist", allow_from=["19995550001"])
|
||||||
|
params = _dm_envelope(source_number="+19995550001")
|
||||||
|
await ch._handle_receive_notification(params)
|
||||||
|
assert len(handled) == 1
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dm_allowlist_matches_with_plus_prefix(self):
|
||||||
|
"""An allowlist entry with '+' must match a sender without '+'."""
|
||||||
|
ch, handled = self._make_dm_channel(policy="allowlist", allow_from=["+19995550001"])
|
||||||
|
params = _dm_envelope(source_number="+19995550001", source_uuid=None)
|
||||||
|
# Replace envelope's sourceNumber with the non-prefixed form by editing
|
||||||
|
# the constructed dict directly so _collect_sender_id_parts sees it.
|
||||||
|
params["envelope"]["sourceNumber"] = "19995550001"
|
||||||
|
await ch._handle_receive_notification(params)
|
||||||
|
assert len(handled) == 1
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dm_allowlist_matches_uuid_case_insensitive(self):
|
||||||
|
"""UUID matching must be case-insensitive."""
|
||||||
|
uuid = "ABCDEF12-3456-7890-ABCD-EF1234567890"
|
||||||
|
ch, handled = self._make_dm_channel(
|
||||||
|
policy="allowlist", allow_from=[uuid.lower()]
|
||||||
|
)
|
||||||
|
params = _dm_envelope(source_number="+19995550001", source_uuid=uuid)
|
||||||
|
await ch._handle_receive_notification(params)
|
||||||
|
assert len(handled) == 1
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dm_disabled_rejected(self):
|
async def test_dm_disabled_rejected(self):
|
||||||
ch = _make_channel(dm_enabled=False)
|
ch = _make_channel(dm_enabled=False)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user