Plano de Fix — #07 SQL Injection (Header api-company-target)
Data: 2026-07-08
Severidade: Crítico
Vetor: Header api-company-target — schema injetado em queries PostgreSQL
1. Resumo
O header HTTP api-company-target é lido de todas as requisições e usado como schema de banco PostgreSQL em queries Dapper e EF Core. Sem validação, um atacante pode injetar SQL arbitrário.
Exemplo de payload:
GET /api/product HTTP/1.1
api-company-target: arezzo"; DROP TABLE "Carts"; --
Schema injetado torna-se:
SELECT * FROM "arezzo"; DROP TABLE "Carts"; -- "."Products"
Serviços afetados: cart, checkout, product, store, backoffice, integration, message, payments-reports, showcase, jobs, bff.
Único serviço NÃO afetado: coezzion-service-auth — usa apenas JWT claim schema, não lê o header.
2. Causa Raiz
2.1 UserProvider.GetSchemaName() — zero validação
Arquivo: coezzion-nuget-common/src/Coezzion.Common/Providers/UserProvider.cs:64-68
if (http.Request.Headers.TryGetValue("api-company-target", out StringValues headerSchema)
&& headerSchema.Any())
{
return headerSchema.ToString(); // ← RAW, sem validação
}
Resolução de schema (ordem de prioridade):
| Prioridade | Fonte | Validação |
|---|---|---|
| 1 | x-api-key == venda_ai → hardcoded "arezzo" | Seguro (hardcoded) |
| 2 | JWT claim schema | Nenhuma |
| 3 | Header api-company-target | NENHUMA ← Vulnerabilidade |
| 4 | SchemaNameScopedWrapper.SchemaName | Nenhuma |
2.2 Leituras diretas do header (bypass UserProvider)
22 localizações em 9 serviços leem api-company-target diretamente de IHttpContextAccessor.HttpContext.Request.Headers, sem passar por GetSchemaName(). Mesmo após o fix central, esses pontos continuariam vulneráveis.
| Serviço | Arquivo | Linhas |
|---|---|---|
| payments-reports | PaymentRepository.cs | 293, 367 |
| payments-reports | CartRepository.cs | 245, 322 |
| payments-reports | IntegrationRepository.cs | 110 |
| payments-reports | CreateCodePaymentsForIntegrations.cs | 20 |
| payments-reports | PaymentCreatedEventHandler.cs | 22 |
| payments-reports | DatabaseConfig.cs | 112 |
| showcase | UserRepository.cs | 22 |
| showcase | PushNotificationService.cs | 26 |
| showcase | DatabaseConfig.cs | 60 |
| integration | DatabaseConfig.cs | 95 |
| integration | PaymentCreatedEventHandler.cs | 22 |
| integration | ChangeCartDataEventHandler.cs | 24 |
| integration | UpdateStatusPaymentEventHandler.cs | 27 |
| message | DatabaseConfig.cs | 44 |
| product | DatabaseConfig.cs | 78 |
| product | ProductAppService.cs (gRPC) | 31 |
| backoffice | DatabaseConfig.cs | 100 |
| store | DatabaseConfig.cs | 79 |
2.3 SQL injection via string interpolation
Todas as queries usam string interpolation $"..." para injetar o schema. Dapper parâmetros @param protegem apenas valores de dados, não identificadores (schema, tabela, coluna).
// Padrão vulnerável (repetido ~200x):
var query = $"SELECT * FROM \"{schema}\".\"Carts\" WHERE ...";
return await connection.QueryAsync<Cart>(query, new { id });
2.4 EF Core — MultiTenantContextProvider
Todos os serviços (exceto auth) configuram o DbContext com schema dinâmico via MultiTenantContextProvider<DbContext>(connectionString, schema). O schema controla o search_path do PostgreSQL — qualquer query LINQ nas entidades tenant usa o schema injetado.
2.5 Cache key injection
~12 decorators de cache usam schema na construção da chave. Schema malicioso → chave arbitrária → leitura/escrita de cache de tenant errado.
3. Decisões de Design
| # | Decisão | Escolha | Motivo |
|---|---|---|---|
| 1 | Whitelist | Hardcode "arezzo" — único schema válido | Simples, imediato. Evolução futura: GetAllOrganizationSchemas() do banco Organizations |
| 2 | Regex | ^[a-z][a-z0-9_]*$ como primeira barreira | Defense-in-depth. Bloqueia aspas, ponto-e-vírgula, hífen, espaços, maiúsculas |
| 3 | Ponto de fix | Centralizar validação em UserProvider.GetSchemaName() | Única alteração = proteção de todos os ~80 call sites de uma vez |
| 4 | Leituras diretas | Migrar para IUserProvider.GetSchemaName() | Elimina bypass. Cada serviço substitui leitura direta por injeção de IUserProvider |
| 5 | Dapper | Manter string interpolation com schema validado | Parametrização @param não protege identificadores SQL. Schema validado é seguro |
| 6 | BFF propagation | Sem validação adicional | HttpClientDelegatingHandlerSchema repassa header; downstream valida |
| 7 | Mitigação profunda | Desabilitar IncludeErrorDetail em staging/prod | Evita leak de nomes de schema e estrutura do banco via mensagens de erro PostgreSQL |
| 8 | Webhook schema | Fora do escopo | Webhook Braspag/PagarMe parseia schema de MerchantOrderId do gateway (input externo). Card separado |
4. Fix no Backend
4.1 UserProvider.cs — Adicionar validação de schema
Arquivo: coezzion-nuget-common/src/Coezzion.Common/Providers/UserProvider.cs
Antes (L64-68):
if (http.Request.Headers.TryGetValue("api-company-target", out StringValues headerSchema)
&& headerSchema.Any())
{
return headerSchema.ToString();
}
Depois:
if (http.Request.Headers.TryGetValue("api-company-target", out StringValues headerSchema)
&& headerSchema.Any())
{
var schema = headerSchema.ToString();
if (!IsValidSchema(schema))
throw new InvalidOperationException($"Schema '{schema}' não autorizado.");
return schema;
}
Novo método privado:
private static readonly Regex SchemaRegex = new("^[a-z][a-z0-9_]*$", RegexOptions.Compiled);
private static readonly HashSet<string> AllowedSchemas = new(StringComparer.Ordinal) { "arezzo" };
private static bool IsValidSchema(string schema)
{
if (string.IsNullOrWhiteSpace(schema))
return false;
if (schema.Length > 63)
return false;
if (!SchemaRegex.IsMatch(schema))
return false;
return AllowedSchemas.Contains(schema);
}
Nota: InvalidOperationException é preferível a SecurityException pois:
- Serviços de background (event handlers, jobs) podem não ter middleware de exceção adequado
- Resulta em HTTP 500 — atacante não distingue "schema inválido" de "erro interno" (sem vazamento de informação)
4.2 Leituras diretas — Migrar para IUserProvider
Cada um dos 22 pontos de leitura direta deve ser substituído por IUserProvider.GetSchemaName().
Estratégia por tipo de leitura:
Tipo A — DatabaseConfig.GetSchema():
Substituir leitura direta do header por injeção de IUserProvider:
// Antes (exemplo: Product.API/Configuration/DatabaseConfig.cs):
schemaName = contextAccessor.HttpContext.Request.Headers["api-company-target"].ToString();
// Depois:
var userProvider = provider.GetService<IUserProvider>();
schemaName = userProvider?.GetSchemaName() ?? "arezzo";
Serviços afetados: product, store, backoffice, integration, message, payments-reports, showcase — 8 arquivos DatabaseConfig.cs.
Tipo B — Event handlers (background):
// Antes (exemplo: PaymentCreatedEventHandler.cs):
var schema = _contextAccessor.HttpContext.Request.Headers["api-company-target"].ToString();
// Depois:
var schema = _userProvider.GetSchemaName();
Serviços afetados: payments-reports, integration — 5 event handlers.
Tipo C — Repositórios com leitura no construtor:
// Antes (exemplo: showcase UserRepository.cs):
_schema = _contextAccessor.HttpContext?.Request.Headers["api-company-target"] ?? "arezzo";
// Depois — injetar IUserProvider como parâmetro de construtor:
_schema = _userProvider.GetSchemaName();
Serviços afetados: showcase, payments-reports — 5 repositórios.
Tipo D — gRPC (ProductAppService.cs):
// Antes:
var schema = context.RequestHeaders.GetValue("api-company-target");
// Depois — injetar IUserProvider e validar no interceptador gRPC:
// (IUserProvider requer HttpContext, que não existe em chamadas gRPC puras.
// Solução: middleware/interceptador gRPC que popula SchemaNameScopedWrapper)
4.3 Cache key — Mitigação automática
Após o fix em GetSchemaName(), todos os decorators de cache que chamam _userProvider.GetSchemaName() passam a usar schema validado. Nenhuma alteração adicional necessária.
Arquivos protegidos automaticamente (~12):
CacheStoreRepositoryDecorator.cs(cart, store)CacheUserReadRepositoryDecorator.cs(cart)CacheSellerRepositoryDecorator.cs(store)CacheBrandRepositoryDecorator.cs(store)CacheChannelRepositoryDecorator.cs(store)CacheSellIndicatorRepositoryDecorator.cs(backoffice)CacheStoreReadRepositoryDecorator.cs(backoffice)CacheThemePredefinitionRepositoryDecorator.cs(backoffice)
4.4 Mitigação profunda — IncludeErrorDetail
Em todos os serviços, garantir que IncludeErrorDetail esteja desabilitado em staging/produção:
// appsettings.Staging.json / appsettings.Production.json:
{
"ErrorHandling": {
"IncludeErrorDetail": false
}
}
Ou via variável de ambiente:
ErrorHandling__IncludeErrorDetail=false
Isso evita que mensagens de erro do PostgreSQL (que podem conter nomes de tabela, schema, estrutura do banco) vazem para o cliente em caso de exceção.
5. Testes de Verificação
| # | Teste | Entrada | Esperado |
|---|---|---|---|
| 1 | Schema válido arezzo | api-company-target: arezzo | Schema retornado "arezzo" |
| 2 | Schema com aspas | api-company-target: arezzo" | InvalidOperationException |
| 3 | Schema com ; | api-company-target: arezzo; DROP TABLE | InvalidOperationException |
| 4 | Schema com -- | api-company-target: arezzo-- | InvalidOperationException |
| 5 | Schema vazio | api-company-target: | InvalidOperationException |
| 6 | Schema maiúsculo | api-company-target: AREZZO | InvalidOperationException |
| 7 | Schema com espaço | api-company-target: arezzo brizza | InvalidOperationException |
| 8 | Schema com hífen | api-company-target: arezzo-co | InvalidOperationException |
| 9 | Schema > 63 caracteres | api-company-target: aaaa... (64 chars) | InvalidOperationException |
| 10 | Schema não whitelisted | api-company-target: brizza | InvalidOperationException |
5.1 Teste de integração (opcional)
[Fact]
public async Task ProductEndpoint_WithMaliciousSchemaHeader_Returns500()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("api-company-target", "arezzo\"; DROP TABLE \"Carts\"; --");
var response = await client.GetAsync("/api/product/search?term=test");
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
6. Fora do Escopo
| Item | Motivo | Quando |
|---|---|---|
#30 (header sobrescreve claim JWT schema) | Card separado | Após #07 |
GetAllOrganizationSchemas() whitelist dinâmica | Evolução futura do whitelist | Quando novos tenants forem adicionados |
| Webhook schema dinâmico (Braspag/PagarMe) | Input externo, não usa api-company-target | Card separado |
| BFF validação de schema antes de repassar | Defense-in-depth, não bloqueia vetor | Avaliar após #07 |
| Refatoração multi-tenancy (connection string por tenant) | Mudança arquitetural grande | Roadmap futuro |
| Rate limit / WAF para bloquear payloads de injeção | Camada adicional, não substitui fix | Infra/sec team |
7. Arquivos de Contexto
| Arquivo | Conteúdo |
|---|---|
context/07-sql-inject-analysis.md | Análise detalhada do vetor, stack trace conceitual, todos os call sites |
context/grill-decisions.md | Decisões do grill com trade-offs |
context/services-matrix.md | Matriz serviço × tipo de leitura × tipo de query × operação DML |