From 4bc7ca13203b6ded7517d7720e692f661c7345f0 Mon Sep 17 00:00:00 2001 From: viettranx Date: Sat, 18 Apr 2026 22:49:48 +0700 Subject: [PATCH] fix(store): tenant-scope GetContactsBySenderIDs to prevent cross-tenant leak --- internal/store/pg/channel_contacts.go | 10 ++++++++-- internal/store/sqlitestore/contacts.go | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/store/pg/channel_contacts.go b/internal/store/pg/channel_contacts.go index 807123fd..2563b090 100644 --- a/internal/store/pg/channel_contacts.go +++ b/internal/store/pg/channel_contacts.go @@ -152,14 +152,20 @@ func (s *PGContactStore) GetContactsBySenderIDs(ctx context.Context, senderIDs [ placeholders[i] = fmt.Sprintf("$%d", i+1) args[i] = id } + // Tenant scope: without this filter a low-priv user in tenant A could probe + // arbitrary sender_ids and exfiltrate displayName/username from tenant B. + // Sibling reads (GetContactByID, ListMergedContacts) already scope this way. + tid := store.TenantIDFromContext(ctx) + args = append(args, tid) + tenantPH := fmt.Sprintf("$%d", len(args)) query := fmt.Sprintf(`SELECT DISTINCT ON (sender_id) id, channel_type, channel_instance, sender_id, user_id, display_name, username, avatar_url, peer_kind, contact_type, thread_id, thread_type, merged_id, first_seen_at, last_seen_at FROM channel_contacts - WHERE sender_id IN (%s) - ORDER BY sender_id, last_seen_at DESC`, strings.Join(placeholders, ",")) + WHERE sender_id IN (%s) AND tenant_id = %s + ORDER BY sender_id, last_seen_at DESC`, strings.Join(placeholders, ","), tenantPH) rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { diff --git a/internal/store/sqlitestore/contacts.go b/internal/store/sqlitestore/contacts.go index afc6c334..a48aabe7 100644 --- a/internal/store/sqlitestore/contacts.go +++ b/internal/store/sqlitestore/contacts.go @@ -155,11 +155,15 @@ func (s *SQLiteContactStore) GetContactsBySenderIDs(ctx context.Context, senderI placeholders[i] = "?" args[i] = id } + // Tenant scope — parity with PG store. Prevents cross-tenant probing of + // displayName/username by a tenant-A user supplying tenant-B sender_ids. + tid := store.TenantIDFromContext(ctx) + args = append(args, tid) // SQLite has no DISTINCT ON; emulate with GROUP BY + MAX rowid trick via subquery query := `SELECT ` + contactSelectCols + ` FROM channel_contacts - WHERE sender_id IN (` + strings.Join(placeholders, ",") + `) + WHERE sender_id IN (` + strings.Join(placeholders, ",") + `) AND tenant_id = ? GROUP BY sender_id ORDER BY sender_id, last_seen_at DESC`