Skip to main content

Análise Detalhada — #08 Cross-Tenant Data Leak

Data: 2026-07-08 Vetor: GET/POST/DELETE /api/payment/profile-fraud + endpoints /api/payment/* sem JWT


1. O Ataque

1.1 ProfileFraudController — zero auth

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

[Route("api/payment/profile-fraud")]
[ApiController]
public class ProfileFraudController(IZZMediator mediator) : BaseController
{
[HttpPost]
public async Task<IActionResult> CreateProfileFraud([FromBody] CreateProfileFraudCommand command) { }

[HttpDelete]
public async Task<IActionResult> DeleteProfileFraud([FromBody] DeleteProfileFraudCommand command) { }

[HttpGet]
public async Task<IActionResult> GetProfilesFraud([FromQuery] FindProfilesFraudQuery query) { }

[HttpGet("status")]
public async Task<IActionResult> GetProfileFraudById([FromQuery] FindProfileFraudStatusQuery query) { }

[HttpGet("log")]
public async Task<IActionResult> GetProfilesFraudLog([FromQuery] FindProfilesFraudLogQuery query) { }
}

Zero [Authorize], zero [UseApiKey], zero [AddSchema]. Completamente público.

1.2 Fluxo de dados

GET /api/payment/profile-fraud?storeId=0


ProfileFraudQueryHandler
│ FindProfilesFraudQuery → ProfileFraudDTO

CustomerProfileFraudRepository.GetCustomersProfileFraudParamsAsync(0, 0)
│ EF Core LINQ: WHERE storeId == 0 OR x.StoreId == storeId
│ storeId=0 → ALL rows do schema atual

Para cada CustomerProfileFraudModel:
│ _orderRepository.GetCustomerByIdAsync(customerId) → Customer (CPF, Name, CellPhone)
│ _storeRepository.GetStoreAsync(storeId) → Store (StoreName, ShowName, CodeStore)

ProfileFraudDTO → 238 registros com PII completo

1.3 DTO exposto

// Checkout.Domain/DTO/ProfileFraud/ProfileFraudDTO.cs
public class ProfileFraudData
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string CustomerCPF { get; set; } // ← PII
public string CustomerName { get; set; } // ← PII
public string CustomerCellPhone { get; set; } // ← PII
public int StoreId { get; set; }
public string StoreName { get; set; }
public string ShowName { get; set; }
public string CodeStore { get; set; }
public bool Blocked { get; set; }
public bool Loyalty { get; set; }
}

1.4 DELETE — bypass do sistema de fraude

// ProfileFraudCommandHandler.cs
public async Task<BaseResult> Handle(DeleteProfileFraudCommand command, CancellationToken cancellationToken)
{
var profileFraud = await _customerProfileFraudRepository
.GetCustomersProfileFraudParamsAsync(command.Id);

profileFraud.DeleteEntity(); // soft delete
await _customerProfileFraudRepository.UnitOfWork.CommitAsync();
}

Atacante pode remover qualquer perfil de fraude de qualquer schema. Sem rastreamento de quem fez a remoção (sem UserActionId no delete).


2. Tenant Selection

2.1 UserProvider.GetSchemaName() — resolução

Arquivo: coezzion-nuget-common/src/Coezzion.Common/Providers/UserProvider.cs

Prioridade de resolução do schema:

PriFonteQuando usado
1x-api-key == venda_ai"arezzo"Requisições com chave venda_ai
2JWT claim schemaRequisições autenticadas
3Header api-company-targetFallback — atacante controla
4SchemaNameScopedWrapper.SchemaNameÚltimo recurso

Sem JWT (prioridade 2 ausente) → cai na prioridade 3 → header api-company-target → tenant arbitrário.

2.2 DatabaseConfig.GetSchema() — via EF Core

Arquivo: coezzion-service-checkout/src/Checkout.API/Configuration/DatabaseConfig.cs

public static string GetSchema(IServiceProvider provider)
{
var schemaName = "";
var contextAccessor = provider.GetService<IHttpContextAccessor>();

if (contextAccessor.HttpContext != null)
{
var identity = contextAccessor.HttpContext.User?.Identity as ClaimsIdentity;
if (identity?.FindFirst("schema") != null)
schemaName = identity.FindFirst("schema").Value;

// fallback para UserProvider (que cai no header)
if (string.IsNullOrEmpty(schemaName))
{
var userProvider = provider.GetService<IUserProvider>();
schemaName = userProvider.GetSchemaName();
}
}
else
{
// background: SchemaNameScopedWrapper
}

return schemaName;
}

Sem JWT → identity.FindFirst("schema") retorna null → fallback para IUserProvider.GetSchemaName() → cai no header.


3. Clientes: zzapp e zzportal

3.1 zzapp (Flutter)

Profile-fraud: código comentado (desabilitado no app). Método findProfileFraud() em profile_fraud_controller.dart só chama update(). Função isCustomerBlocked() retorna hardcoded false.

Endpoints ativos chamados pelo zzapp:

EndpointMétodoArquivo
/api/payment-report/report/sales/{orderId}/5GETcart_link_controller.dart
/api/payment-report/dashboard/appGETomni_indicators_link_controller.dart
/api/store/payment/{storeId}/appGETstore_payment_config_controller.dart

Auth: AuthInterceptor adiciona Authorization: Bearer {token} em todas as requests.

3.2 zzportal (React)

Profile-fraud: ativamente usado.

EndpointMétodoArquivo
/api/payment/profile-fraudGET, POST, DELETEsrc/services/payment/index.ts
/api/payment/approvePOSTsrc/services/payment/index.ts
/api/payment/reversePOSTsrc/services/payment/index.ts
/api/payment/cancelPOSTsrc/services/payment/index.ts
/api/payment/antifraud/retry/{id}POSTsrc/services/payment/index.ts
/api/payment/history/{id}GETsrc/services/payment/index.ts
/api/payment/control-operation/storePUT, POST, DELETEsrc/services/payment/index.ts
/api/payment/control-operationPUTsrc/services/payment/index.ts

Auth: api.defaults.headers.common.Authorization = Bearer {token} setado no sign-in, refresh, e rehydration. Todas requests do portal enviam Bearer.

3.3 Conclusão

Ambos clientes já enviam Authorization: Bearer em todas as requests. Adicionar [Authorize(JwtBearer)] no backend não causa breaking change.


4. Endpoints Afetados (Checkout Service)

4.1 ProfileFraudController — 5 actions, zero auth

ActionHTTPDado
CreateProfileFraudPOSTCria perfil de fraude
DeleteProfileFraudDELETERemove perfil de fraude
GetProfilesFraudGETLista perfis com PII
GetProfileFraudByIdGETQuery por storeId+customerId
GetProfilesFraudLogGETLog de modificações

4.2 PaymentController — 6 actions sem JWT

ActionHTTPAuth atualDado
GetOrderHistoryGETNenhumHistórico de pedido
CreatePaymentPOST[AddSchema]Cria pagamento
GetInfoAsyncGET[AddSchema]Dados do pagamento
GetInstallmentsDetailsAsyncGET[AddSchema]Parcelamento
GetReasonsScoreAsyncGET[AddSchema]Motivos por score
NPSAsyncPOST[AddSchema]Pesquisa NPS

4.3 Já protegidos (sem mudança)

ActionAuth
ApprovePaymentJWT ✅
ReversePaymentJWT ✅
CancelOrderJWT ✅
FinishOrderPaymentsScheme JWT ✅
RetryAsyncPaymentsScheme JWT ✅
AddRecommendedItemPaymentsScheme JWT ✅
RemoveRecommendedItemPaymentsScheme JWT ✅
ControlOperationController (todos)JWT ✅

5. Cross-Tenant Binding

Após adicionar JWT:

  1. UserProvider.GetSchemaName() encontra claim schema no JWT (prioridade 2)
  2. Retorna o schema do token — não do header
  3. Header api-company-target é ignorado (prioridade 3 nunca alcançada)
  4. Fix do #07 (schema whitelist) atua como defense-in-depth: mesmo que o header seja lido, só "arezzo" passa

Exemplo: Usuário logado na Arezzo (JWT claim schema: arezzo) faz request com header api-company-target: brizza. O backend usa arezzo (do JWT), ignora brizza (do header).