Isolation: no user directory on a shared instance (contacts-only visibility, no account enumeration) #1199

Open
opened 2026-10-06 10:08:25 +00:00 by kayg · 2 comments
Owner

Cross-user isolation (owner, 2026-10-06): no user discovery on a shared instance

calternal.cloud is ONE instance shared by unrelated customers (Individual / Duo / Family plans). Owner: "make sure since it's all one instance, they can't discover other people."

Current behaviour: GET /api/v1/files/people ("List other Users") returns EVERY enabled User other than the caller to any signed-in User with the data scope — a full directory of all customers on a shared instance. Any other path that reveals other accounts (share dialog autocomplete, mentions, Group member pickers, invite flows saying "this person already has an account", avatars/names in shared-item metadata, error messages, timing) is in scope.

Do:

  1. Instance setting directory_visibility (admin, config + Admin UI, documented in contracts/config.json):
    • instance — current behaviour; default for self-hosted Instances (a family/team server).
    • contacts — default for the official shared instance (calternal.cloud). A User can see only: members of Groups they belong to (a Duo/Family plan auto-creates its member Group), people they already share with or who share with them, and Admins acting in Admin UI.
  2. Exact lookup without enumeration in contacts mode: the Share dialog accepts a full email address (or exact handle); the server answers identically whether or not an account exists ("We'll share it with this address"), shares directly when the account exists, otherwise sends/queues an invite; never reveals existence, name or avatar before the recipient accepts; rate-limited per User; constant-time-ish responses.
  3. Audit every route returning other Users' identities (names, emails, avatars, presence) and apply the policy; error messages never confirm an account.
  4. Tests: the cross-user isolation matrix (#331 probe) extended — two unrelated Users on a contacts Instance cannot list, autocomplete, mention or infer each other through any route; Group members can; sharing by exact email works both when the account exists and not, with indistinguishable responses. Add the probe to tests/adversarial.
    This is a merge/launch blocker for calternal.cloud. Gates per crate + bun run check + bun run test quoted; an independent security-style review follows (word it as a defensive audit).
## Cross-user isolation (owner, 2026-10-06): no user discovery on a shared instance calternal.cloud is ONE instance shared by unrelated customers (Individual / Duo / Family plans). Owner: "make sure since it's all one instance, they can't discover other people." Current behaviour: `GET /api/v1/files/people` ("List other Users") returns EVERY enabled User other than the caller to any signed-in User with the data scope — a full directory of all customers on a shared instance. Any other path that reveals other accounts (share dialog autocomplete, mentions, Group member pickers, invite flows saying "this person already has an account", avatars/names in shared-item metadata, error messages, timing) is in scope. Do: 1. **Instance setting `directory_visibility`** (admin, config + Admin UI, documented in contracts/config.json): - `instance` — current behaviour; default for self-hosted Instances (a family/team server). - `contacts` — default for the official shared instance (calternal.cloud). A User can see only: members of Groups they belong to (a Duo/Family plan auto-creates its member Group), people they already share with or who share with them, and Admins acting in Admin UI. 2. **Exact lookup without enumeration** in `contacts` mode: the Share dialog accepts a full email address (or exact handle); the server answers identically whether or not an account exists ("We'll share it with this address"), shares directly when the account exists, otherwise sends/queues an invite; never reveals existence, name or avatar before the recipient accepts; rate-limited per User; constant-time-ish responses. 3. Audit every route returning other Users' identities (names, emails, avatars, presence) and apply the policy; error messages never confirm an account. 4. Tests: the cross-user isolation matrix (#331 probe) extended — two unrelated Users on a `contacts` Instance cannot list, autocomplete, mention or infer each other through any route; Group members can; sharing by exact email works both when the account exists and not, with indistinguishable responses. Add the probe to tests/adversarial. This is a merge/launch blocker for calternal.cloud. Gates per crate + `bun run check` + `bun run test` quoted; an independent security-style review follows (word it as a defensive audit).
Author
Owner

Starting #1199 on branch job/directory-1199, based on origin/dev at 5301e020859c1d61b4a7f812a5049a64241203e8. I am tracing identity-returning routes and the shared instance config/admin flows before implementing contacts-only visibility and indistinguishable exact-address sharing.

Starting #1199 on branch `job/directory-1199`, based on `origin/dev` at `5301e020859c1d61b4a7f812a5049a64241203e8`. I am tracing identity-returning routes and the shared instance config/admin flows before implementing contacts-only visibility and indistinguishable exact-address sharing.
Author
Owner

Finding: crates/plugins/files/src/lookup.rs::people selects every enabled User other than the caller. crates/calternal-server/src/wire/groups.rs::list_groups returns every active Group to a data-scoped User, and the Share dialog consumes both lists. In contacts mode the People result must be limited to contacts, and Group names/picker results must not enumerate unrelated Groups. The admin Users and Group-membership routes are guarded by the admin scope.

Finding: `crates/plugins/files/src/lookup.rs::people` selects every enabled User other than the caller. `crates/calternal-server/src/wire/groups.rs::list_groups` returns every active Group to a data-scoped User, and the Share dialog consumes both lists. In contacts mode the People result must be limited to contacts, and Group names/picker results must not enumerate unrelated Groups. The admin Users and Group-membership routes are guarded by the admin scope.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#1199
No description provided.