From 29230c3b61ee6cf18f13768432695592d4a7eadb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 04:35:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20[security=20fix]=20Implement=20s?= =?UTF-8?q?trict=20tenant=20isolation=20in=20webhooks=20and=20interaction?= =?UTF-8?q?=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced unfiltered `db.query(Tenant).first()` with secure lookups based on Evolution API instance name and sender's WhatsApp JID. - Modified `InteractionService.handle_message` to filter content jobs by the sender's authorized tenant. - Ensured new tenants created via webhooks are correctly associated with their Evolution API instances and admins. - Prevents cross-tenant data access and unauthorized command execution. Co-authored-by: CaioIsCoding <106751132+CaioIsCoding@users.noreply.github.com> --- app/api/v1/webhooks.py | 25 +++++++++++++++++++++++-- app/services/interaction.py | 6 ++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/api/v1/webhooks.py b/app/api/v1/webhooks.py index 0d46bb1..c6b40a0 100644 --- a/app/api/v1/webhooks.py +++ b/app/api/v1/webhooks.py @@ -36,15 +36,36 @@ async def evolution_webhook(request: Request, db: Session = Depends(get_db)): if not media_url: media_url = "https://images.unsplash.com/photo-1523275335684-37898b6baf30" - tenant = db.query(Tenant).first() + # Identify Tenant securely + instance_name = payload.get("instance") + tenant = db.query(Tenant).join(SocialAccount).filter( + SocialAccount.platform == "whatsapp", + SocialAccount.external_id == instance_name + ).first() + + if not tenant: + # Fallback to remote_jid lookup if instance not linked yet + tenant = db.query(Tenant).filter(Tenant.admin_jids.contains([remote_jid])).first() + if not tenant: + # For this exercise, create a tenant if not found, but associate with the sender tenant = Tenant( - business_name="Default Business", + business_name=f"Business {instance_name or 'Default'}", niche="E-commerce", admin_jids=[remote_jid], # Add the sender as admin for testing required_approvals=2 ) db.add(tenant) + db.flush() + + if instance_name: + social = SocialAccount( + tenant_id=tenant.id, + platform="whatsapp", + external_id=instance_name + ) + db.add(social) + db.commit() db.refresh(tenant) diff --git a/app/services/interaction.py b/app/services/interaction.py index 02d3216..4d8a759 100644 --- a/app/services/interaction.py +++ b/app/services/interaction.py @@ -16,8 +16,10 @@ async def handle_message(self, remote_jid: str, text: str): cmd = text.strip().lower() # Find the most recent job for this sender or related tenant - # In a real multi-tenant app, we'd filter by sender's association to a tenant - job = self.db.query(ContentJob).order_by(desc(ContentJob.created_at)).first() + # Filtered by sender's association to a tenant for security (Ticket 🔒 Tenant Isolation) + job = self.db.query(ContentJob).join(Tenant).filter( + Tenant.admin_jids.contains([remote_jid]) + ).order_by(desc(ContentJob.created_at)).first() if not job: return {"status": "error", "message": "No job found"}