Skip to content

security(signal): scope pairing approvals to accountId#26639

Closed
bmendonca3 wants to merge 2 commits intoopenclaw:mainfrom
bmendonca3:bm/signal-pairing-account-scope-20260225
Closed

security(signal): scope pairing approvals to accountId#26639
bmendonca3 wants to merge 2 commits intoopenclaw:mainfrom
bmendonca3:bm/signal-pairing-account-scope-20260225

Conversation

@bmendonca3
Copy link
Copy Markdown
Contributor

@bmendonca3 bmendonca3 commented Feb 25, 2026

Summary

  • Fixes a Signal multi-account authz boundary bug where pairing-store approvals were read and written without account scoping.
  • Pairing-store lookups now include accountId for Signal inbound authorization.
  • Pairing request writes now include accountId so approvals remain isolated per Signal account.
  • Adds focused regression tests for both read and write paths.

Change Type

  • Security fix
  • Tests

Scope

  • src/signal/monitor/event-handler.ts
  • src/signal/monitor/event-handler.account-scope.test.ts

Security Impact

  • Boundary crossed (before fix): pairing approvals in Signal account A affected DM authorization in Signal account B.
  • Practical impact: a sender approved in one Signal account could be implicitly trusted in another account on the same gateway.
  • Worst case: cross-account unauthorized DM-triggered execution and session contamination across account boundaries.

Repro + Verification

  1. Configure multiple Signal accounts (for example default and work) on one gateway.
  2. Pair sender X on account default.
  3. Send DM from sender X to account work.
  4. Before fix: work can treat X as allowlisted due to unscoped pairing store reads/writes.
  5. After fix: work requires its own approval for X.

Targeted verification:

pnpm exec vitest run src/signal/monitor/event-handler.account-scope.test.ts --maxWorkers=1
pnpm exec vitest run src/signal/monitor/event-handler.inbound-contract.test.ts src/signal/monitor/event-handler.mention-gating.test.ts --maxWorkers=1
pnpm exec oxfmt --check src/signal/monitor/event-handler.ts src/signal/monitor/event-handler.account-scope.test.ts

Evidence

Human Verification

  • Pair sender X on Signal account A; confirm DMs to A are authorized.
  • Confirm the same sender X is blocked or prompted for pairing on Signal account B until approved for B.
  • Pair X on B; confirm DMs to B then succeed.

Compatibility / Migration

  • No schema/config migration.
  • Existing single-account setups are unchanged.
  • Multi-account setups become correctly isolated per account.

Failure Recovery

  • Revert this commit to restore previous behavior.
  • Operational fallback: explicitly approve required senders per account while triaging.

Risks and Mitigations

  • Risk: stricter account isolation may block senders previously (incorrectly) trusted cross-account.
  • Mitigation: pairing workflow is unchanged per account; regression tests cover both read and write isolation paths.

Greptile Summary

Adds accountId parameter to Signal pairing store read and write operations to isolate pairing approvals between multi-account Signal setups.

Changes:

  • readChannelAllowFromStore now called with accountId parameter (line 447 in event-handler.ts)
  • upsertChannelPairingRequest now includes accountId in the request metadata (line 463)
  • Added regression tests verifying account-scoped reads and writes

Critical Issue in Underlying Implementation:

While this PR correctly passes accountId to the pairing functions, there is a critical bug in src/pairing/pairing-store.ts:506 (not part of this PR) that undermines the security fix:

The upsertChannelPairingRequest function finds existing pairing requests using:

const existingIdx = reqs.findIndex((r) => r.id === id);

This matches on id only, ignoring accountId. When the same sender pairs with multiple accounts:

  1. Sender X pairs with account default → creates {id: X, meta: {accountId: "default"}, code: "AAAA"}
  2. Sender X pairs with account work → finds existing entry by id, updates to {id: X, meta: {accountId: "work"}, code: "AAAA"}
  3. The pairing for default is lost — trying to approve code AAAA for account default will fail

The findIndex should use requestMatchesAccountId (like the approval flow does on line 590) to ensure pairing requests are isolated per account.

Impact:

  • Multi-account setups cannot pair the same sender with different accounts
  • Second pairing overwrites the first account's pairing request
  • The security boundary is only partially enforced

Confidence Score: 2/5

  • This PR partially addresses the security issue but is undermined by a critical bug in the underlying pairing store implementation
  • The changes in this PR are correct (passing accountId to pairing functions), but a critical bug in src/pairing/pairing-store.ts prevents proper account isolation. The upsertChannelPairingRequest function matches pairing requests by id only (line 506), not by id + accountId, causing the second account's pairing to overwrite the first. Tests pass because they mock the pairing functions and don't catch the real issue. The security fix is incomplete.
  • Pay close attention to src/pairing/pairing-store.ts:506 (not in this PR) which needs to match pairing requests by both id and accountId using the requestMatchesAccountId helper

Last reviewed commit: dc78132

Copy link
Copy Markdown

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix — scoping readChannelAllowFromStore and upsertChannelPairingRequest to accountId is the right approach. Without this, multi-account Signal setups (e.g. "work" and "personal") would share a single pairing store, letting a user who paired with one account access the other. Tests properly assert the account-scoped key propagation in both the read and upsert paths. LGTM.

@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 27, 2026
@openclaw-barnacle openclaw-barnacle bot removed the agents Agent runtime and tooling label Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: signal Channel integration: signal size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants