Skip to main content

Plano de Fix — #08 Vazamento Cross-Tenant (Profile Fraud)

Data: 2026-07-08 Severidade: Crítico Endpoints: GET/POST/DELETE /api/payment/profile-fraud e endpoints /api/payment/* sem JWT


1. Resumo

O ProfileFraudController e 6 ações do PaymentController sob /api/payment/* não exigem autenticação. Um atacante com x-api-key + header api-company-target acessa e modifica dados de qualquer tenant.

Vetor #08 — profile-fraud:

GET /api/payment/profile-fraud?storeId=0
Header: x-api-key: 9D235B9B-E9FA-4B7A-BAB9-6F7B1A822EE6
Header: api-company-target: arezzo

→ 200 OK, 67 KB JSON com 238 perfis:
CPF, nome completo, celular, loja de cada cliente bloqueado

DELETE também passa:

DELETE /api/payment/profile-fraud
Body: {"id": 1}
Headers: x-api-key + api-company-target: arezzo

→ Atacante remove suspeitos legítimos da blacklist de fraude

Schemas acessíveis com mesma chave: arezzo (200), schutz (503), anacapri (503).


2. Causa Raiz

2.1 Endpoints sem [Authorize]

ProfileFraudControllerzero auth. Nenhum atributo de autorização no controller nem nas 5 actions.

// ProfileFraudController.cs:10-12
[Route("api/payment/profile-fraud")]
[ApiController]
public class ProfileFraudController(IZZMediator mediator) : BaseController
{
[HttpGet] // ← sem [Authorize]
public async Task<IActionResult> GetProfilesFraud(...)

PaymentController — 6 ações públicas (sem JWT):

ActionAuth atual
GetOrderHistoryNenhum
CreatePayment[AddSchema]
GetInfoAsync[AddSchema]
GetInstallmentsDetailsAsync[AddSchema]
GetReasonsScoreAsync[AddSchema]
NPSAsync[AddSchema]

2.2 [AddSchema] não é autenticação

AddSchemaAttribute implementa IAuthorizationFilter mas só valida presença do header api-company-target — não autentica o caller:

// AddSchemaAttribute.cs:10-11
if (!context.HttpContext.Request.Headers.ContainsKey("api-company-target") ||
string.IsNullOrEmpty(context.HttpContext.Request.Headers["api-company-target"].ToString()))
{
// return 401 ← não é auth, é guarda de schema
}

2.3 Header api-company-target controla o tenant

Sem JWT, UserProvider.GetSchemaName() cai no header (prioridade 3). Schema arbitrário → tenant arbitrário.


3. Decisões de Design

#DecisãoEscolhaMotivo
1Auth mechanism[Authorize(JwtBearer)] — mesmo padrão de approve, reverse, cancelAmbos clientes (zzapp, zzportal) já enviam Authorization: Bearer em todas as requests
2EscopoApenas endpoints chamados por zzapp + zzportalWebhooks, PagarMe, Ecommerce têm callers diferentes (gateways, público não autenticado)
3GranularidadeController-level no ProfileFraudController; action-level nos 6 do PaymentControllerPaymentController tem mix de auth (alguns já JWT, outros PaymentsScheme, outros API key)
4Cross-tenant bindingAutomático com JWT — claim schema dita o tenantGetSchemaName() prioridade 2 (JWT claim) vence prioridade 3 (header). Fix do #07 adiciona defense-in-depth
5[AddSchema] mantido?Sim, como guarda adicionalJá existente. Defense-in-depth: exige header presente + schema validado (#07) + JWT válido
6Breaking change?Nãozzapp envia Bearer via AuthInterceptor (todas requests). zzportal envia Bearer via api.defaults.headers.common

4. Mudanças no Backend

4.1 ProfileFraudController.cs — Adicionar [Authorize] no controller

Arquivo: coezzion-service-checkout/src/Checkout.API/Controllers/ProfileFraudController.cs

Antes:

[Route("api/payment/profile-fraud")]
[ApiController]
public class ProfileFraudController(IZZMediator mediator) : BaseController

Depois:

[Route("api/payment/profile-fraud")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public class ProfileFraudController(IZZMediator mediator) : BaseController

Protege as 5 actions de uma vez: GetProfilesFraud, CreateProfileFraud, DeleteProfileFraud, GetProfileFraudById, GetProfilesFraudLog.

4.2 PaymentController.cs — Adicionar [Authorize] em 6 ações

Arquivo: coezzion-service-checkout/src/Checkout.API/Controllers/PaymentController.cs

GetOrderHistory — atualmente sem auth

// Antes:
[HttpGet("history/{id}")]
public async Task<IActionResult> GetOrderHistory(int id)

// Depois:
[HttpGet("history/{id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetOrderHistory(int id)

CreatePayment — atualmente só [AddSchema]

// Antes:
[HttpPost("v2/payment")]
[AddSchema]
public async Task<IActionResult> CreatePayment([FromBody] CreatePaymentInputModel input)

// Depois:
[HttpPost("v2/payment")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AddSchema]
public async Task<IActionResult> CreatePayment([FromBody] CreatePaymentInputModel input)

GetInfoAsync — atualmente só [AddSchema]

// Antes:
[HttpGet("v2/{id}")]
[AddSchema]
public async Task<IActionResult> GetInfoAsync(int id)

// Depois:
[HttpGet("v2/{id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AddSchema]
public async Task<IActionResult> GetInfoAsync(int id)

GetInstallmentsDetailsAsync — atualmente só [AddSchema]

// Antes:
[HttpGet("v2/{id}/installments")]
[AddSchema]
public async Task<IActionResult> GetInstallmentsDetailsAsync(int id)

// Depois:
[HttpGet("v2/{id}/installments")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AddSchema]
public async Task<IActionResult> GetInstallmentsDetailsAsync(int id)

GetReasonsScoreAsync — atualmente só [AddSchema]

// Antes:
[HttpGet("v2/reasons/{score}")]
[AddSchema]
public async Task<IActionResult> GetReasonsScoreAsync(int score)

// Depois:
[HttpGet("v2/reasons/{score}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AddSchema]
public async Task<IActionResult> GetReasonsScoreAsync(int score)

NPSAsync — atualmente só [AddSchema]

// Antes:
[HttpPost("v2/nps")]
[AddSchema]
public async Task<IActionResult> NPSAsync([FromBody] NpsInputModel input)

// Depois:
[HttpPost("v2/nps")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AddSchema]
public async Task<IActionResult> NPSAsync([FromBody] NpsInputModel input)

4.3 Endpoints que NÃO mudam

ControllerActionsAuth atualMotivo
PaymentControllerApprovePayment, ReversePayment, CancelOrder, AntifraudRetryJWT ✅Já protegidos
PaymentControllerFinishOrder, RetryAsync, AddRecommendedItem, RemoveRecommendedItemPaymentsScheme JWT ✅Já protegidos
PaymentControllerGetPaymentStatus, CheckPaymentFlwPaymentAPI KeyCallers externos (venda_ai, internal)
EcommerceControllerTodosVariadoEcommerce público — fora do escopo
WebhookControllerTodosNenhumGateways externos (Braspag, ClearSale, PagarMe)
PagarMeControllerTodosAPI Key (internal)Gateway de pagamento
ControlOperationControllerTodosJWT ✅Já protegidos
CrmBonusControllerUpdateBonusToken[AddSchema]Não usado por zzapp/zzportal
PdvControllerGeneratePayloadPdv[AddSchema]Não usado por zzapp/zzportal
SchemaControllerTodosNenhum[ApiExplorerSettings(IgnoreApi = true)] — endpoint interno

5. Verificação de Segurança

#TesteEsperado
1GET /api/payment/profile-fraud sem JWT401 Unauthorized
2GET /api/payment/profile-fraud com JWT válido200 OK (dados do tenant do JWT)
3POST /api/payment/profile-fraud sem JWT401 Unauthorized
4DELETE /api/payment/profile-fraud sem JWT401 Unauthorized
5GET /api/payment/profile-fraud com JWT + api-company-target: brizza diferente do claim schema: arezzoClaim vence → dados da Arezzo (não cross-tenant)
6GET /api/payment/history/{id} sem JWT401 Unauthorized
7POST /api/payment/v2/payment sem JWT401 Unauthorized
8GET /api/payment/v2/{id} sem JWT401 Unauthorized
9GET /api/payment/v2/{id}/installments sem JWT401 Unauthorized
10GET /api/payment/v2/reasons/{score} sem JWT401 Unauthorized
11POST /api/payment/v2/nps sem JWT401 Unauthorized
12GET /api/payment/history/{id} com JWT válido200 OK
13POST /api/payment/profile-fraud com JWT válido + body válido200 OK (cria perfil no tenant do JWT)
14DELETE /api/payment/profile-fraud com JWT válido + id existente200 OK (remove do tenant do JWT)

6. Fora do Escopo

ItemMotivoQuando
Ecommerce endpoints (/api/payment/ecommerce/*)Callers não autenticados (ecommerce público)Card separado
Webhook endpoints (/api/payment/webhook/*)Callers são gateways externos (Braspag, ClearSale, PagarMe)Card separado
PagarMe endpointsUsam [UseApiKey("internal")] — mecanismo diferenteCard separado
CrmBonusController, PdvControllerNão usados por zzapp/zzportalCard separado se necessário
Endpoints x-api-key only (GetPaymentStatus, CheckPaymentFlwPayment)Callers externos legítimos (venda_ai) sem JWTAvaliar caso a caso
#30 (JWT schema claim vs header)Vulnerabilidade distintaCard separado

7. Arquivos de Contexto

ArquivoConteúdo
context/08-cross-tenant-analysis.mdAnálise detalhada do ataque, modelo de dados, fluxo de PII
context/grill-decisions.mdDecisões do grill com trade-offs
context/client-auth-verification.mdVerificação de que zzapp e zzportal já enviam Bearer