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]
ProfileFraudController — zero 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):
| Action | Auth atual |
|---|---|
GetOrderHistory | Nenhum |
CreatePayment | Só [AddSchema] |
GetInfoAsync | Só [AddSchema] |
GetInstallmentsDetailsAsync | Só [AddSchema] |
GetReasonsScoreAsync | Só [AddSchema] |
NPSAsync | Só [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ão | Escolha | Motivo |
|---|---|---|---|
| 1 | Auth mechanism | [Authorize(JwtBearer)] — mesmo padrão de approve, reverse, cancel | Ambos clientes (zzapp, zzportal) já enviam Authorization: Bearer em todas as requests |
| 2 | Escopo | Apenas endpoints chamados por zzapp + zzportal | Webhooks, PagarMe, Ecommerce têm callers diferentes (gateways, público não autenticado) |
| 3 | Granularidade | Controller-level no ProfileFraudController; action-level nos 6 do PaymentController | PaymentController tem mix de auth (alguns já JWT, outros PaymentsScheme, outros API key) |
| 4 | Cross-tenant binding | Automático com JWT — claim schema dita o tenant | GetSchemaName() prioridade 2 (JWT claim) vence prioridade 3 (header). Fix do #07 adiciona defense-in-depth |
| 5 | [AddSchema] mantido? | Sim, como guarda adicional | Já existente. Defense-in-depth: exige header presente + schema validado (#07) + JWT válido |
| 6 | Breaking change? | Não | zzapp 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
| Controller | Actions | Auth atual | Motivo |
|---|---|---|---|
PaymentController | ApprovePayment, ReversePayment, CancelOrder, AntifraudRetry | JWT ✅ | Já protegidos |
PaymentController | FinishOrder, RetryAsync, AddRecommendedItem, RemoveRecommendedItem | PaymentsScheme JWT ✅ | Já protegidos |
PaymentController | GetPaymentStatus, CheckPaymentFlwPayment | API Key | Callers externos (venda_ai, internal) |
EcommerceController | Todos | Variado | Ecommerce público — fora do escopo |
WebhookController | Todos | Nenhum | Gateways externos (Braspag, ClearSale, PagarMe) |
PagarMeController | Todos | API Key (internal) | Gateway de pagamento |
ControlOperationController | Todos | JWT ✅ | Já protegidos |
CrmBonusController | UpdateBonusToken | [AddSchema] só | Não usado por zzapp/zzportal |
PdvController | GeneratePayloadPdv | [AddSchema] só | Não usado por zzapp/zzportal |
SchemaController | Todos | Nenhum | [ApiExplorerSettings(IgnoreApi = true)] — endpoint interno |
5. Verificação de Segurança
| # | Teste | Esperado |
|---|---|---|
| 1 | GET /api/payment/profile-fraud sem JWT | 401 Unauthorized |
| 2 | GET /api/payment/profile-fraud com JWT válido | 200 OK (dados do tenant do JWT) |
| 3 | POST /api/payment/profile-fraud sem JWT | 401 Unauthorized |
| 4 | DELETE /api/payment/profile-fraud sem JWT | 401 Unauthorized |
| 5 | GET /api/payment/profile-fraud com JWT + api-company-target: brizza diferente do claim schema: arezzo | Claim vence → dados da Arezzo (não cross-tenant) |
| 6 | GET /api/payment/history/{id} sem JWT | 401 Unauthorized |
| 7 | POST /api/payment/v2/payment sem JWT | 401 Unauthorized |
| 8 | GET /api/payment/v2/{id} sem JWT | 401 Unauthorized |
| 9 | GET /api/payment/v2/{id}/installments sem JWT | 401 Unauthorized |
| 10 | GET /api/payment/v2/reasons/{score} sem JWT | 401 Unauthorized |
| 11 | POST /api/payment/v2/nps sem JWT | 401 Unauthorized |
| 12 | GET /api/payment/history/{id} com JWT válido | 200 OK |
| 13 | POST /api/payment/profile-fraud com JWT válido + body válido | 200 OK (cria perfil no tenant do JWT) |
| 14 | DELETE /api/payment/profile-fraud com JWT válido + id existente | 200 OK (remove do tenant do JWT) |
6. Fora do Escopo
| Item | Motivo | Quando |
|---|---|---|
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 endpoints | Usam [UseApiKey("internal")] — mecanismo diferente | Card separado |
CrmBonusController, PdvController | Não usados por zzapp/zzportal | Card separado se necessário |
Endpoints x-api-key only (GetPaymentStatus, CheckPaymentFlwPayment) | Callers externos legítimos (venda_ai) sem JWT | Avaliar caso a caso |
| #30 (JWT schema claim vs header) | Vulnerabilidade distinta | Card separado |
7. Arquivos de Contexto
| Arquivo | Conteúdo |
|---|---|
context/08-cross-tenant-analysis.md | Análise detalhada do ataque, modelo de dados, fluxo de PII |
context/grill-decisions.md | Decisões do grill com trade-offs |
context/client-auth-verification.md | Verificação de que zzapp e zzportal já enviam Bearer |