Skip to main content

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):

PrioridadeFonteValidação
1x-api-key == venda_ai → hardcoded "arezzo"Seguro (hardcoded)
2JWT claim schemaNenhuma
3Header api-company-targetNENHUMA ← Vulnerabilidade
4SchemaNameScopedWrapper.SchemaNameNenhuma

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çoArquivoLinhas
payments-reportsPaymentRepository.cs293, 367
payments-reportsCartRepository.cs245, 322
payments-reportsIntegrationRepository.cs110
payments-reportsCreateCodePaymentsForIntegrations.cs20
payments-reportsPaymentCreatedEventHandler.cs22
payments-reportsDatabaseConfig.cs112
showcaseUserRepository.cs22
showcasePushNotificationService.cs26
showcaseDatabaseConfig.cs60
integrationDatabaseConfig.cs95
integrationPaymentCreatedEventHandler.cs22
integrationChangeCartDataEventHandler.cs24
integrationUpdateStatusPaymentEventHandler.cs27
messageDatabaseConfig.cs44
productDatabaseConfig.cs78
productProductAppService.cs (gRPC)31
backofficeDatabaseConfig.cs100
storeDatabaseConfig.cs79

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ãoEscolhaMotivo
1WhitelistHardcode "arezzo" — único schema válidoSimples, imediato. Evolução futura: GetAllOrganizationSchemas() do banco Organizations
2Regex^[a-z][a-z0-9_]*$ como primeira barreiraDefense-in-depth. Bloqueia aspas, ponto-e-vírgula, hífen, espaços, maiúsculas
3Ponto de fixCentralizar validação em UserProvider.GetSchemaName()Única alteração = proteção de todos os ~80 call sites de uma vez
4Leituras diretasMigrar para IUserProvider.GetSchemaName()Elimina bypass. Cada serviço substitui leitura direta por injeção de IUserProvider
5DapperManter string interpolation com schema validadoParametrização @param não protege identificadores SQL. Schema validado é seguro
6BFF propagationSem validação adicionalHttpClientDelegatingHandlerSchema repassa header; downstream valida
7Mitigação profundaDesabilitar IncludeErrorDetail em staging/prodEvita leak de nomes de schema e estrutura do banco via mensagens de erro PostgreSQL
8Webhook schemaFora do escopoWebhook 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

#TesteEntradaEsperado
1Schema válido arezzoapi-company-target: arezzoSchema retornado "arezzo"
2Schema com aspasapi-company-target: arezzo"InvalidOperationException
3Schema com ;api-company-target: arezzo; DROP TABLEInvalidOperationException
4Schema com --api-company-target: arezzo--InvalidOperationException
5Schema vazioapi-company-target: InvalidOperationException
6Schema maiúsculoapi-company-target: AREZZOInvalidOperationException
7Schema com espaçoapi-company-target: arezzo brizzaInvalidOperationException
8Schema com hífenapi-company-target: arezzo-coInvalidOperationException
9Schema > 63 caracteresapi-company-target: aaaa... (64 chars)InvalidOperationException
10Schema não whitelistedapi-company-target: brizzaInvalidOperationException

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

ItemMotivoQuando
#30 (header sobrescreve claim JWT schema)Card separadoApós #07
GetAllOrganizationSchemas() whitelist dinâmicaEvolução futura do whitelistQuando novos tenants forem adicionados
Webhook schema dinâmico (Braspag/PagarMe)Input externo, não usa api-company-targetCard separado
BFF validação de schema antes de repassarDefense-in-depth, não bloqueia vetorAvaliar após #07
Refatoração multi-tenancy (connection string por tenant)Mudança arquitetural grandeRoadmap futuro
Rate limit / WAF para bloquear payloads de injeçãoCamada adicional, não substitui fixInfra/sec team

7. Arquivos de Contexto

ArquivoConteúdo
context/07-sql-inject-analysis.mdAnálise detalhada do vetor, stack trace conceitual, todos os call sites
context/grill-decisions.mdDecisões do grill com trade-offs
context/services-matrix.mdMatriz serviço × tipo de leitura × tipo de query × operação DML