Injeção de SQL: como o ataque funciona e como blindar sua aplicação
Como a injeção de SQL nasce da concatenação de entrada não confiável, os payloads que exploram a falha e a defesa que encerra a classe: queries parametrizadas.
Neste artigo
Uma tela de login pergunta usuário e senha e monta a consulta juntando os campos direto na string: SELECT FROM usuarios WHERE email = ' mais o que a pessoa digitou mais ' AND senha = ' mais a senha mais '. Parece inofensivo até alguém digitar, no campo de email, admin@site.com' --. A query resultante vira SELECT FROM usuarios WHERE email = 'admin@site.com' --' AND senha = ''. O -- inicia um comentário em SQL, e tudo depois dele desaparece. A verificação de senha some. O atacante entra como administrador sem saber a senha. Nenhum firewall foi burlado, nenhuma criptografia foi quebrada — o banco de dados simplesmente obedeceu a um comando que o programa, sem querer, montou para ele.
Injeção de SQL é uma das falhas mais antigas e mais devastadoras da segurança de aplicações, e continua no topo das listas de risco depois de mais de vinte anos. Não é um problema de bancos "inseguros" nem de linguagens ruins; é um problema estrutural de como muitos programas conversam com o banco. Entender a raiz exata é o que permite fechar a classe inteira, em vez de tapar um buraco de cada vez.
Por que a injeção existe#
A causa é uma só: o interpretador de SQL não distingue código de dado quando eles chegam misturados na mesma string. Quando você concatena a entrada do usuário dentro do texto da consulta, você está pedindo ao banco que interprete aquilo como parte do comando. Se a entrada contiver caracteres que têm significado em SQL — aspas, hífens, ponto e vírgula, a palavra OR — o banco os trata como sintaxe, não como valor. O programa achava que estava passando um email; o banco recebeu uma instrução.
Essa é a mesma raiz de toda injeção — de comando, de LDAP, de template. Um canal que deveria carregar apenas dado acaba carregando controle. E a fronteira entre "dado do usuário" e "comando do programa" desaparece no momento exato da concatenação. Por isso soluções paliativas que tentam "limpar" a entrada são frágeis: elas tentam adivinhar quais caracteres são perigosos num contexto que tem muitas formas de escapar da adivinhação.
Anatomia dos payloads#
O ' OR '1'='1 é o cartão de visitas da injeção. Injetado num filtro WHERE email = '...', ele transforma a condição em algo sempre verdadeiro, fazendo a query retornar todas as linhas em vez de uma. Combinado com o comentário --, ele descarta o resto da consulta original.
A partir daí, o ataque escala. Na injeção UNION-based, o atacante anexa um UNION SELECT à consulta para trazer colunas de outras tabelas — tipicamente extraindo nomes de usuário e hashes de senha de uma tabela que a query original nem tocava. Requer descobrir o número e o tipo das colunas, o que é feito por tentativa e erro sistemática. Na injeção error-based, o atacante provoca erros de banco deliberadamente para que a mensagem de erro vaze fragmentos de dados — mais um motivo para nunca devolver o erro cru do driver ao cliente.
Quando a aplicação não devolve resultados nem erros visíveis, o atacante recorre à injeção cega (blind). Na variante booleana, ele faz perguntas de sim ou não — "a primeira letra da senha do admin é maior que 'm'?" — e observa a diferença de comportamento da página entre verdadeiro e falso, extraindo o dado bit a bit. Na variante baseada em tempo, quando nem o comportamento muda, o atacante injeta uma função de espera como SLEEP condicional: se a resposta demora, a condição era verdadeira. É lento, mas totalmente automatizável, e exfiltra bancos inteiros dado a dado.
A defesa primária: queries parametrizadas#
A defesa que encerra a classe não é filtrar caracteres — é nunca misturar código e dado na mesma string. Consultas parametrizadas, também chamadas de prepared statements, resolvem isso de forma estrutural. Em vez de montar WHERE email = ' mais a entrada mais ', você escreve a consulta com um marcador de posição — WHERE email = ? ou WHERE email = $1 — e entrega o valor do email por um canal separado. O banco recebe primeiro a estrutura da consulta, compila o plano de execução com os marcadores no lugar, e só então recebe os valores. Como o plano já está fixado, nenhum caractere no valor pode mais mudar a estrutura do comando. Aspas, hífens e OR viram apenas conteúdo literal do campo — exatamente o que deveriam ser.
A diferença é conceitual e absoluta: no modelo concatenado, o dado vira parte do comando; no modelo parametrizado, o dado nunca alcança o parser como sintaxe. Não é uma questão de "escapar melhor" — é de os dois nunca se encontrarem no mesmo lugar. Por isso queries parametrizadas são a defesa recomendada em qualquer padrão de código seguro, e por isso concatenar entrada em query é vetado em bases de código sérias, sem exceção por "era mais rápido assim".
ORMs e query builders: ajuda, não bala de prata#
Frameworks de mapeamento objeto-relacional (ORM) e query builders geralmente usam parametrização por baixo dos panos, e por isso reduzem muito a superfície de injeção. Mas eles não são imunes. Quase todo ORM oferece uma saída para raw queries — SQL cru para casos que o abstrator não cobre — e nesse ponto a responsabilidade volta para você: se você concatenar entrada dentro de um raw query, o ORM não vai te salvar. Cláusulas dinâmicas como um IN com uma lista de tamanho variável, ou ordenação por coluna escolhida pelo usuário, são pontos onde desenvolvedores frequentemente voltam a concatenar sem perceber. Usar o ORM não dispensa entender o que ele faz; dispensa apenas o trabalho repetitivo quando você usa o caminho parametrizado dele.
Onde parametrizar não alcança#
Há partes da consulta que não podem ser parametrizadas porque não são valores, e sim identificadores: o nome de uma coluna num ORDER BY, o nome de uma tabela, a direção ASC/DESC. Um marcador de posição serve para valores, não para estrutura. Se você permite que o usuário escolha por qual coluna ordenar, não dá para fazer bind do nome da coluna. A defesa aqui é uma allowlist estrita: o valor recebido do cliente é comparado contra uma lista fechada de identificadores válidos, e qualquer coisa fora dela é rejeitada. Você nunca constrói o nome da coluna a partir do texto do usuário; você usa o texto do usuário apenas para escolher dentre nomes que você mesmo escreveu.
Camadas adicionais#
Nenhuma defesa séria depende de uma única barreira. Além da parametrização, o privilégio mínimo do usuário de banco que a aplicação usa limita o estrago: se aquele usuário não tem permissão de DROP, uma injeção bem-sucedida não derruba tabelas; se ele só enxerga o schema da aplicação, não alcança dados de outro sistema no mesmo servidor. Um WAF (firewall de aplicação) pode barrar payloads conhecidos e é útil como camada, mas nunca como cura — ele opera por padrões e é contornável; confiar nele como defesa principal é construir sobre areia. E não devolver o erro cru do banco ao cliente fecha o canal que a injeção error-based explora.
Injeção de segunda ordem#
Um caso que engana até quem já parametriza o caminho óbvio é a injeção de segunda ordem. O atacante grava um valor aparentemente inofensivo — um nome de usuário contendo aspas e SQL — que é armazenado com segurança porque a escrita foi parametrizada. O perigo aparece depois, quando outra parte do sistema lê aquele valor do banco e o concatena numa nova query, confiando nele por já estar "dentro de casa". A lição é que a origem do dado não muda a regra: qualquer valor que entra numa query, venha do usuário agora ou do seu próprio banco depois, passa por parâmetro. Dado é dado, e nenhum dado é confiável a ponto de virar código.
Por que "escapar aspas na mão" falha#
A tentação de resolver injeção substituindo aspas simples por aspas duplicadas, ou filtrando a palavra SELECT, parece funcionar nos testes e falha na produção. Há dezenas de formas de codificar caracteres, variações de sintaxe entre bancos, comentários que quebram filtros de palavra-chave, e contextos numéricos onde nem aspas são necessárias para injetar. Escapar manualmente é uma corrida armamentista que o defensor perde, porque precisa acertar sempre e o atacante precisa acertar uma vez. Parametrização não entra nessa corrida: ela remove o campo de batalha.
O caso das cláusulas dinâmicas#
Boa parte das injeções que sobrevivem em bases de código que já adotaram parametrização mora nas cláusulas que parecem exigir montagem dinâmica. Um filtro de busca avançada que aceita uma quantidade variável de condições, uma cláusula IN cujo número de elementos muda a cada chamada, uma paginação com ordenação escolhida pelo usuário — todos convidam o desenvolvedor a voltar a concatenar "só desta vez". A saída correta continua sendo parametrização, mas exige um pouco mais de trabalho: para o IN de tamanho variável, você gera o número certo de marcadores de posição programaticamente e faz bind de cada valor, sem jamais interpolar os valores no texto; para a ordenação, você mapeia a escolha do usuário contra uma allowlist de colunas conhecidas e usa o resultado da allowlist, nunca o texto do usuário. O princípio é invariável: valores sempre por parâmetro, estrutura sempre por código que você escreveu. No instante em que um pedaço de query passa a depender de texto do cliente para definir estrutura, você está de volta ao território da injeção, e nenhuma quantidade de esperteza na montagem da string compensa isso.
Vale também desmontar um argumento comum de quem resiste à parametrização por medo de performance. Prepared statements não são mais lentos na prática relevante; ao contrário, bancos reaproveitam o plano de execução compilado entre chamadas com a mesma estrutura, o que frequentemente os torna mais rápidos sob carga, além de mais seguros. A ideia de que concatenar é otimização é um mito que sobrevive porque soa plausível e nunca é medido. Segurança e desempenho, neste caso, apontam para o mesmo lado.
Como testar e revisar#
Na prática, a melhor detecção começa no code review com uma pergunta mecânica: existe alguma query montada por concatenação, formatação de string ou interpolação com dado que não seja constante do código? Ferramentas de análise estática (SAST) marcam esses pontos automaticamente e cabem bem num gate de CI. Ferramentas dinâmicas e scanners especializados testam os endpoints com payloads de injeção e observam as respostas. E um pentest confirma, rota por rota, campo por campo, que nenhuma entrada hostil produz erro de banco, coerção de tipo ou vazamento. Mas a verdade libertadora é que, se toda consulta da sua base usa parâmetros e toda cláusula estrutural usa allowlist, a injeção de SQL deixa de ser uma preocupação recorrente e vira uma classe fechada. Não é um bug que você conserta; é uma categoria que você elimina por construção.
