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
OrganizationsviaGetAllOrganizationSchemas()+ 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
DbCommandBuildercomQuoteIdentifier - 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:
InvalidOperationExceptionSecurityException- 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.