Skip to content

fix(gateway): match SimpleX allowlist on local alias, not spoofable profile name - #41246

Open
tanebrae-yz wants to merge 1 commit into
NousResearch:mainfrom
tanebrae-yz:fix/simplex-allowlist-name-spoofing
Open

tanebrae-yz wants to merge 1 commit into
NousResearch:mainfrom
tanebrae-yz:fix/simplex-allowlist-name-spoofing

Conversation

@tanebrae-yz

Copy link
Copy Markdown

What does this PR do?

I noticed the SimpleX allowlist could be bypassed by anyone who can message
the bot. A recent change taught SIMPLEX_ALLOWED_USERS to accept display
names (490c486), but the name it matched on — source.user_name — is the
contact's self-asserted profile display name. That field is fully
controlled by the remote contact and isn't unique, so if an operator listed
a name in the allowlist (which the docs actively encourage, since the SimpleX
UI never shows the numeric contactId), any other contact could just rename
their own profile to that string and get full agent access. This was turning
a stable-ID check into a guess-the-name check where the attacker controls
both sides.

The fix keeps the name convenience but anchors it to something the remote
party can't forge: the contact's local display name — the alias the
SimpleX CLI assigns and deduplicates locally (alice, alice_1, ...). The
adapter now carries that alias in user_id_alt (the field already used for
stable platform alt-IDs like the Signal UUID and Feishu union_id), and the
gateway matches the allowlist against that instead of the profile name. A
later contact who renames their profile to a taken alias gets alice_1
locally, so they can't collide with a trusted contact. The numeric contactId
via user_id still works and stays the most robust option.

Related Issue

N/A

Type of Change

  • 🔒 Security fix

Changes Made

  • gateway/run.py: in _is_user_authorized, match SIMPLEX_ALLOWED_USERS
    against source.user_id_alt (the stable local alias) instead of
    source.user_name (the attacker-controlled profile display name).
  • plugins/platforms/simplex/adapter.py: capture localDisplayName for both
    DM contacts and group members and pass it through build_source as
    user_id_alt; update the module docstring and the interactive-setup prompt.
  • tests/gateway/test_unauthorized_dm_behavior.py: rework the display-name
    test to authorize via the local alias and add a regression test proving a
    spoofed profile name is rejected.
  • website/docs/user-guide/messaging/simplex.md: clarify that the allowlist
    matches the local alias / contactId, not the spoofable profile name.

How to Test

  1. scripts/run_tests.sh tests/gateway/test_unauthorized_dm_behavior.py
    34 pass, including the new test_simplex_allowlist_rejects_spoofed_profile_name.
  2. scripts/run_tests.sh tests/gateway/test_simplex_plugin.py — 28 pass.
  3. Repro before the fix: set SIMPLEX_ALLOWED_USERS=alice, message the bot
    from an untrusted contact whose profile name is "alice" but whose local
    alias is "alice_1" — old code authorized them, new code denies them.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run the gateway tests and they pass
  • I've added tests for my changes (required for bug fixes)
  • I've tested on my platform: macOS 15.5

Documentation & Housekeeping

  • I've updated relevant documentation (docs/, docstrings)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

…rofile name

## What does this PR do?

I noticed the SimpleX allowlist could be bypassed by anyone who can message
the bot. A recent change taught `SIMPLEX_ALLOWED_USERS` to accept display
names (490c486), but the name it matched on — `source.user_name` — is the
contact's *self-asserted profile display name*. That field is fully
controlled by the remote contact and isn't unique, so if an operator listed
a name in the allowlist (which the docs actively encourage, since the SimpleX
UI never shows the numeric contactId), any other contact could just rename
their own profile to that string and get full agent access. This was turning
a stable-ID check into a guess-the-name check where the attacker controls
both sides.

The fix keeps the name convenience but anchors it to something the remote
party can't forge: the contact's *local* display name — the alias the
SimpleX CLI assigns and deduplicates locally (`alice`, `alice_1`, ...). The
adapter now carries that alias in `user_id_alt` (the field already used for
stable platform alt-IDs like the Signal UUID and Feishu union_id), and the
gateway matches the allowlist against that instead of the profile name. A
later contact who renames their profile to a taken alias gets `alice_1`
locally, so they can't collide with a trusted contact. The numeric contactId
via `user_id` still works and stays the most robust option.

## Related Issue

N/A

## Type of Change

- [x] 🔒 Security fix

## Changes Made

- `gateway/run.py`: in `_is_user_authorized`, match `SIMPLEX_ALLOWED_USERS`
  against `source.user_id_alt` (the stable local alias) instead of
  `source.user_name` (the attacker-controlled profile display name).
- `plugins/platforms/simplex/adapter.py`: capture `localDisplayName` for both
  DM contacts and group members and pass it through `build_source` as
  `user_id_alt`; update the module docstring and the interactive-setup prompt.
- `tests/gateway/test_unauthorized_dm_behavior.py`: rework the display-name
  test to authorize via the local alias and add a regression test proving a
  spoofed profile name is rejected.
- `website/docs/user-guide/messaging/simplex.md`: clarify that the allowlist
  matches the local alias / contactId, not the spoofable profile name.

## How to Test

1. `scripts/run_tests.sh tests/gateway/test_unauthorized_dm_behavior.py` —
   34 pass, including the new `test_simplex_allowlist_rejects_spoofed_profile_name`.
2. `scripts/run_tests.sh tests/gateway/test_simplex_plugin.py` — 28 pass.
3. Repro before the fix: set `SIMPLEX_ALLOWED_USERS=alice`, message the bot
   from an untrusted contact whose profile name is "alice" but whose local
   alias is "alice_1" — old code authorized them, new code denies them.

## Checklist

### Code

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits (`fix(scope):`, etc.)
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix (no unrelated commits)
- [x] I've run the gateway tests and they pass
- [x] I've added tests for my changes (required for bug fixes)
- [x] I've tested on my platform: macOS 15.5

### Documentation & Housekeeping

- [x] I've updated relevant documentation (docs/, docstrings)
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — N/A
- [x] I've considered cross-platform impact (Windows, macOS)
- [x] I've updated tool descriptions/schemas if I changed tool behavior — N/A
@daimon-nous daimon-nous Bot added type/security Security vulnerability or hardening P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/signal Signal CLI adapter area/auth Authentication, OAuth, credential pools labels Jun 7, 2026
@liuhao1024

Copy link
Copy Markdown
Contributor

Positive verification — security review

Reviewed the full diff (gateway/run.py, plugins/platforms/simplex/adapter.py, tests, docs). No issues found.

Key observations:

  1. Fix is correct. source.user_name (remote-controlled profile displayName) → source.user_id_alt (locally-assigned CLI alias) closes the allowlist spoofing vector. The local alias is deduplicated by the SimpleX CLI (alice, alice_1, ...) and cannot be forced to collide with an existing contact.
  2. Adapter wiring is sound. _handle_new_chat_item extracts localDisplayName from contact/group-member info separately from displayName, and passes it through as user_id_alt=sender_local_name or None in the SessionSource constructor.
  3. Regression test coverage is good. test_simplex_allowlist_rejects_spoofed_profile_name directly asserts that a contact with user_name="hujikuji" but user_id_alt="hujikuji_1" is denied when the allowlist contains hujikuji. The existing test_simplex_allowlist_accepts_local_display_name is updated to set user_name="Totally Legit" (different from the allowlisted value), confirming local-alias matching still works.
  4. Docs updated consistently. simplex.md and the adapter docstring both clarify that only local display names are accepted.

Clean security fix — approving for merge.

@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the focused security fix. The underlying authorization issue is still present on current main: gateway/authz_mixin.py:590-605 adds source.user_name to SimpleX allowlist candidates, while plugins/platforms/simplex/adapter.py:516-518 and :528-530 can derive that name from profile display-name data.

Problems

  • The submitted authorization hunk targets gateway/run.py, but the live implementation was extracted to gateway/authz_mixin.py by a706a349b; likewise, the submitted _handle_new_chat_item target has become _handle_chat_item (plugins/platforms/simplex/adapter.py:472-619). This needs a current-main salvage rather than a direct application.
  • The proposed authorization tests manually construct SessionSource; they do not verify the adapter wiring that must set user_id_alt for current direct and group inbound payloads.

Suggested changes

  • Port the allowlist change into gateway/authz_mixin.py, and preserve localDisplayName separately as user_id_alt in both current adapter branches.
  • Add direct and group adapter-to-authorization regression tests covering a spoofed profile name and a distinct local alias.
  • Align the remaining config/reference descriptions at plugins/platforms/simplex/plugin.yaml:22-24 and website/docs/reference/environment-variables.md:613 if aliases are supported.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/profiles Multi-profile isolation, HERMES_HOME scoping labels Jul 14, 2026
@iiviel

iiviel commented Sep 12, 2026

Copy link
Copy Markdown

I measured this against a real daemon, and the security rationale here does not hold — the local alias can be forced to collide.

Setup: three simplex-chat instances, separate databases, no Hermes in the loop. Attacker and legitimate contact both set profile.displayName = alice. The attacker connects first.

Result at the bot prompt:

> /contacts
alice
alice_1

Each remote sent a marker so the mapping isn't an assumption:

19:35 alice>   MARKER_FROM_MALLORY_THE_ATTACKER
19:36 alice_1> MARKER_FROM_REAL_ALICE

createdAt confirms the order independently — contactId 3 (alice) 28 seconds before contactId 4 (alice_1).

The daemon does deduplicate, but it suffixes the late arrival. The unsuffixed name is first-come-first-served, and the party that arrives first is not the one the operator controls. Auto-accept is on by default and the bot's contact link is public by construction, so the attacker connecting first is the normal setup order, not a race.

Since this PR matches on user_id_alt (= localDisplayName), an allowlist entry of alice would match the attacker and reject the legitimate contact, who is now alice_1. The fix would ship without fixing the reported behaviour.

#44741, which removes the display-name identifier from the authorization set entirely, does not have this problem.

Tested against the v7.0.2 release (simplex-chat --version self-reports SimpleX Chat v7.0.0.12). Worth noting that no simplex-chat version is pinned anywhere in the repo, so any fix resting on daemon naming behaviour rests on something unpinned.


Analysis conducted with AI assistance (Claude, Anthropic); the measurement above is a real run and reproducible from the steps given.

@alt-glitch alt-glitch added comp/plugins Plugin system and bundled plugins and removed platform/signal Signal CLI adapter sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades area/profiles Multi-profile isolation, HERMES_HOME scoping labels Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants