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
@@ -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: {
|
||||
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |