Skip to main content

Grill Decisions — #07 SQL Injection

Data: 2026-07-08


Decisão 1: Mecanismo de whitelist — hardcode vs dinâmico

Pergunta: Como definir quais schemas são válidos? Hardcode uma lista fixa ou carregar do banco via GetAllOrganizationSchemas()?

Opções:

  • Hardcode lista fixa ("arezzo")
  • Carregar do banco Organizations via GetAllOrganizationSchemas() + cache
  • Híbrido (regex + whitelist do banco)

Decisão: Hardcode "arezzo" — único schema válido neste momento.

Trade-off: Simples, zero dependência de banco no startup, uma linha. Quando novos tenants forem adicionados, evoluir para GetAllOrganizationSchemas() — o método já existe em todos os MigrationServices e retorna a lista de schemas da tabela Organizations, filtrada (exclui aws* e pg_temp*).

Plano de evolução:

// Hoje:
private static readonly HashSet<string> AllowedSchemas = new(StringComparer.Ordinal) { "arezzo" };

// Futuro:
public class SchemaWhitelistProvider
{
private ImmutableHashSet<string> _allowedSchemas;

public void LoadFromDatabase(IEnumerable<string> orgSchemas)
{
_allowedSchemas = orgSchemas
.Where(s => !string.IsNullOrEmpty(s) && s.Length <= 63 && SchemaRegex.IsMatch(s))
.ToImmutableHashSet(StringComparer.Ordinal);
}

public bool IsAllowed(string schema) => _allowedSchemas.Contains(schema);
}

Decisão 2: Regex como defense-in-depth

Pergunta: Regex ^[a-z][a-z0-9_]*$ é necessário se já temos whitelist?

Opções:

  • Só whitelist
  • Regex + whitelist

Decisão: Regex + whitelist — duas barreiras independentes.

Trade-off: Regex barra caracteres perigosos (aspas, ponto-e-vírgula, hífen, espaços) antes mesmo do lookup. Custo: RegexOptions.Compiled → compilado uma vez, execução sub-milissegundo. Whitelist garante que mesmo um schema com nome válido (ex: brizza) não passa se não estiver na lista.

Defesa em profundidade: se a whitelist falhar (ex: bug de inicialização), a regex ainda bloqueia injeção. Se a regex falhar (ex: ReDoS — improvável com padrão simples e Compilado), a whitelist ainda bloqueia.


Decisão 3: Centralizar no UserProvider vs middleware

Pergunta: Onde colocar a validação? No UserProvider.GetSchemaName() (biblioteca compartilhada) ou em middleware ASP.NET (por serviço)?

Opções:

  • UserProvider.GetSchemaName() — centralizado na lib comum
  • Middleware ASP.NET — por serviço
  • Ambos

Decisão: UserProvider.GetSchemaName() — fix central na lib comum.

Trade-off: Um fix na nuget Coezzion.Common protege ~80 call sites em todos os serviços que usam IUserProvider. Serviços que leem o header diretamente (22 pontos) precisam ser migrados para usar IUserProvider. Middleware seria redundante e não cobriria cenários onde o schema é lido fora do pipeline HTTP (event handlers, jobs, gRPC).


Decisão 4: Tratamento de schema inválido — exception vs log + default

Pergunta: O que fazer quando o schema é inválido? Lançar exceção ou retornar fallback ("arezzo") com log?

Opções:

  • Lançar InvalidOperationException → HTTP 500
  • Log warning + retornar "arezzo" como default

Decisão: Lançar InvalidOperationException → HTTP 500.

Trade-off: Retornar fallback silencioso esconderia o ataque — atacante receberia 200 OK com dados da Arezzo. Exceção resulta em 500, que é o comportamento esperado para input malicioso. Atacante não consegue distinguir "schema inválido" de "erro interno" (sem leak de informação). Se houver falsos positivos, o erro aparece em log/APM rapidamente.


Decisão 5: Dapper — parametrização vs string interpolation

Pergunta: Dapper suporta @param para valores. Por que não usar parametrização no schema também?

Resposta técnica: PostgreSQL não permite parametrização de identificadores (schema, tabela, coluna). SELECT * FROM @schema.Products gera erro de sintaxe. Schema names precisam ser interpolados como identificadores — daí a necessidade de validação rigorosa antes da interpolação.

Opções:

  • Continuar com string interpolation (schema validado)
  • Refatorar para DbCommandBuilder com QuoteIdentifier
  • Refatorar para EF Core somente (LINQ → sem SQL raw)

Decisão: Continuar com string interpolation + schema validado.

Trade-off: Refatorar ~200 queries para QuoteIdentifier ou EF Core-only é caro e arriscado. Schema validado por regex + whitelist é equivalente em segurança. QuoteIdentifier do Npgsql poderia ser usado como camada adicional, mas o PostgreSQL já exige que o schema exista — se o nome é válido e está na whitelist, a interpolação é segura.


Decisão 6: Webhook schema dinâmico (fora do escopo)

Pergunta: O WebhookBraspagService parseia schema de MerchantOrderId.Split('-')[2].ToLower() — valor vindo do gateway de pagamento. Isso também é SQL injection?

Resposta: Sim, é um vetor distinto. O schema vem do campo MerchantOrderId do gateway (Braspag/PagarMe), não do header api-company-target. O atacante precisaria controlar o MerchantOrderId — o que é mais difícil, mas possível se o gateway não validar.

Decisão: Fora do escopo do #07. Card separado.

Trade-off: Mecanismo diferente, superfície de ataque diferente, dono diferente (integração com gateway). Mesmo fix (whitelist) se aplica, mas o ponto de entrada é outro.


Decisão 7: BFF validation — defense-in-depth

Pergunta: O BFF repassa o header api-company-target sem validar. Deveria validar antes de repassar?

Opções:

  • Validar no BFF também
  • Não validar (downstream valida)

Decisão: Não validar no BFF neste momento.

Trade-off: Validar no BFF seria defense-in-depth — barra antes de chegar no downstream. Mas o BFF não usa o schema para queries, apenas repassa. Adicionar validação no BFF sem o downstream validar seria frágil (basta um chamador pular o BFF). Se o downstream validar (que é o fix principal), a validação no BFF é redundante. Reavaliar após todos os downstreams estarem protegidos.


Decisão 8: InvalidOperationException vs exceção customizada

Pergunta: Qual tipo de exceção lançar para schema inválido?

Opções:

  • InvalidOperationException
  • SecurityException
  • Exceção customizada InvalidSchemaException

Decisão: InvalidOperationException

Trade-off: SecurityException é reservado para violações de segurança do .NET runtime. Exceção customizada exigiria novo tipo na nuget comum + middleware de tratamento em cada serviço. InvalidOperationException é simples, já tratada pelo pipeline padrão como 500, sem distinção para o atacante.