fix(agents): POST /agents nunca verificava tenant de userId/extensionId

Achado numa revisão de segurança sobre o trabalho da PHASE 29: um FK do
Postgres só checa que a linha referenciada existe, não que ela é visível
sob a RLS da sessão atual — então um userId/extensionId de outro tenant
seria aceito silenciosamente no create do Agent (violação do princípio já
seguido em todo o resto do código: nunca confiar em id vindo do client
sem checar contra o tenant do JWT). O frontend já só oferece opções do
próprio tenant, mas isso é conveniência de UI, não autorização.

Corrigido com uma checagem explícita de TenantMembership/Extension antes
do create (400 se não pertencer). Reverificado ponta a ponta: criação
legítima continua funcionando igual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-29 16:37:32 -03:00
parent 75212cafe6
commit 7f56c96454
6 changed files with 38 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
import {
BadRequestException,
Body,
Controller,
Delete,
@@ -36,6 +37,26 @@ export class AgentsController {
);
await assertQuota(tenantId, "maxAgents", activeCount);
// Bug real, achado construindo a tela de Agentes (PHASE 29): nada
// aqui verificava que `dto.userId`/`dto.extensionId` pertencem a este
// tenant antes do create — um FK constraint no Postgres não passa
// pela RLS de visibilidade (só checa existência da linha), então um
// id de outro tenant seria aceito silenciosamente. O frontend já só
// oferece opções do próprio tenant, mas o backend é a autoridade
// (secao 31/146), nunca confia só no que o client filtrou.
const [membership, extension] = await withTenantContext(prisma, tenantId, (tx) =>
Promise.all([
tx.tenantMembership.findUnique({ where: { tenantId_userId: { tenantId, userId: dto.userId } } }),
dto.extensionId ? tx.extension.findFirst({ where: { id: dto.extensionId, tenantId } }) : Promise.resolve(null),
]),
);
if (!membership) {
throw new BadRequestException("userId nao pertence a este tenant");
}
if (dto.extensionId && !extension) {
throw new BadRequestException("extensionId nao pertence a este tenant");
}
const agent = await withTenantContext(prisma, tenantId, (tx) =>
tx.agent.create({
data: {