Skip to content

fix(messages,privacy): stop the People Picker from republishing email-derived handles - #854

Open
catomean wants to merge 1 commit into
mainfrom
fix/messages-picker-email-leak
Open

fix(messages,privacy): stop the People Picker from republishing email-derived handles#854
catomean wants to merge 1 commit into
mainfrom
fix/messages-picker-email-leak

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Summary

Visitor feedback on /messages: the New Message picker's default suggestion list showed accounts like georgy.butaev+ocauth1 @georgy.butaev+ocauth1 — real email local-parts published as public handles, visible to any logged-in user.

This exact class of leak was already fixed at the two known write paths (handle_new_user() trigger, ProfileServerService.ensureProfile() — see supabase/migrations/20260826130000_stop_deriving_usernames_from_email.sql), with an existing, deliberately-manual backfill script (scripts/rename-email-derived-usernames.sql). But:

  1. Two more write paths still minted the same leak, independently of both fixed ones, found by an exhaustive sweep of every place a profiles row gets created or its username/name set:

    • scripts/db/setup-db.ts hardcodes a sign-in as butaeff@gmail.com and upserts username: email.split('@')[0] on every run — almost certainly the actual source of the +ocauth1/+ocauth2 test accounts in the report (butaeff is literally one of them).
    • scripts/db/reset-user-password.js upserts username: email verbatim (the full address) for its fixture account.

    Both now use the same neutral-username shape as the already-fixed paths.

  2. Accounts already leaked and not yet backfilled are still visible today. Added isEmailDerivedHandle() (src/config/public-directory.ts) — same predicate and .invalid system-account exception as the SQL side's count_email_derived_usernames() — and wired it into GET /api/profiles to hide a still-leaking handle from the default (no search term) suggestion list, the exact surface this was reported from. An explicit search still finds the person — hiding someone from a search for their exact name would be an availability regression, not a privacy fix. This is a mitigation, not a substitute for actually renaming the handle, which needs the existing (already-correct, already .invalid-aware) scripts/rename-email-derived-usernames.sql run against production by someone with DB credentials — out of scope for this dispatch, noted in the session handoff (this sandbox has no live Supabase credentials).

  3. Secondary complaint ("double-truncated noise"): NewConversationModal's picker row showed name and @username duplicated whenever a profile has no display name (the title already falls back to the handle). Now the @handle line only renders when it adds information beyond the title.

Deliberately left out of scope

The same investigation surfaced two more email-derived-display-name fallbacks (src/services/timeline/utils/post-composer.ts's optimistic post event, src/components/timeline/PostComposerMobile.tsx, src/components/ui/UserProfileDropdown.tsx) — all client-side, self-view only (only the account's own owner ever sees their own optimistic post or their own account-menu label; not a third-party exposure), so not the reported bug. Noted in the handoff as a follow-up, not fixed here per "address exactly this feedback."

Test plan

  • npx tsc --noEmit — clean
  • npx eslint on all changed files — 0 errors
  • New unit tests for isEmailDerivedHandle() in __tests__/unit/config/public-directory.test.ts: flags a still-leaking handle, case-insensitive, never flags a .invalid system account (the exact bug that took the Cat's own handle down for two days — see 20260828070000_system_accounts_keep_their_handles.sql), false with nothing to compare
  • Full local npm run test:unit (2642 tests) and full pre-push suite — green
  • Could not verify GET /api/profiles's new filter end-to-end against live data — this sandbox has no Supabase credentials, so the query never actually executes here. Verified by code review: email is selected only for the server-side filter and stripped via .map(({email, ...rest}) => rest) before the response is ever serialized — it never reaches the client.

🤖 Generated with Claude Code

…-derived handles

Visitor feedback on /messages: the New Message picker's default
suggestion list showed accounts like "georgy.butaev+ocauth1
@georgy.butaev+ocauth1" — real email local-parts published as public
handles, visible to any logged-in user. This is the leak fixed at the
two known signup paths in 20260826130000_stop_deriving_usernames_from_email.sql
and ProfileServerService.ensureProfile() — but not every account is
through the (deliberately manual, not-automatic) backfill script yet,
and two more write paths still minted the same leak independently:

- scripts/db/setup-db.ts hardcodes a sign-in as butaeff@gmail.com and
  upserts username: email.split('@')[0] on every run — almost
  certainly the actual source of the "+ocauth1"/"+ocauth2" test
  accounts in the report (butaeff is one of them).
- scripts/db/reset-user-password.js upserts username: email verbatim
  (the full address, not just the local part) for its fixture account.

Both now use the same neutral-username shape as the fixed paths
(neutralUsernameFor in the .ts script; the equivalent inlined in the
plain .js one, which can't import it).

For accounts already leaked and not yet backfilled: added
isEmailDerivedHandle() (src/config/public-directory.ts, same predicate
and .invalid exception as the SQL side's count_email_derived_usernames())
and wired it into GET /api/profiles to hide a still-leaking handle from
the *default* (no search term) suggestion list — the surface this was
reported from. An explicit search still finds the person; this isn't a
substitute for renaming their handle, which needs
scripts/rename-email-derived-usernames.sql run against production by
someone with DB access (noted in the session handoff, not in scope for
this dispatch — no live Supabase credentials in this sandbox).

Also: NewConversationModal's picker row showed name and @username
duplicated whenever a profile has no display name (title already falls
back to the handle) — pure noise, worse on narrow screens where both
lines truncate. Now the @handle line only renders when it adds
information beyond the title.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant