From 4afa9bea32cd4538af19cbba45a874dbb614be8d Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Fri, 17 Apr 2026 21:39:33 +0300 Subject: [PATCH] fix(automations): scope meta-send contact lookup by user_id The engine runs with the service-role client (bypassing RLS) and the public POST /api/automations/engine endpoint accepts contact_id from the request body. Without a user_id filter on the contacts lookup, an authenticated user could fire their own automations against another tenant's contact UUID and send via their own WhatsApp config to that contact's phone. UUIDs are unguessable so practical risk is low, but this is cheap defense-in-depth. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/automations/meta-send.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/automations/meta-send.ts b/src/lib/automations/meta-send.ts index ffbc9f4..1df61a2 100644 --- a/src/lib/automations/meta-send.ts +++ b/src/lib/automations/meta-send.ts @@ -52,13 +52,22 @@ type SendInput = async function sendViaMeta(input: SendInput): Promise<{ whatsapp_message_id: string }> { const db = supabaseAdmin() + // Scope the contact lookup by user_id. The engine uses the + // service-role client (bypassing RLS), and the public + // /api/automations/engine endpoint accepts contact_id from the + // request body — without this filter, an authenticated user could + // fire their own automations against another tenant's contact UUID + // and send via their own WhatsApp config to that contact's phone. + // Practical risk is low (UUIDs are unguessable) but the check is + // cheap defense-in-depth. const { data: contact, error: contactErr } = await db .from('contacts') .select('id, phone') .eq('id', input.contactId) + .eq('user_id', input.userId) .maybeSingle() if (contactErr || !contact?.phone) { - throw new Error('contact phone missing') + throw new Error('contact not found for this user') } const sanitized = sanitizePhoneForMeta(contact.phone)