SQL Injection: da vulnerabilidade à proteção por parâmetros
"Injeção de SQL", talvez mais conhecida pelo termo em inglês "SQL Injection", é um dos primeiros problemas que precisamos lidar na programação quando há um banco de dados envolvido. O problema é simples de entender, e consiste no uso de entrada do usuário para realizar algo no banco de dados sem nenhum tipo de preparo ou tratamento.
Introdução ao problema
Para exemplificar, imagine um banco de dados onde as senhas são armazenadas como "texto puro", sem hash e nada do tipo (sabemos que isso não é o correto). Nesse caso, para verificar se um usuário pode acessar o sistema, foi criada uma função que compara o login e senha informados pelo usuário com os dados na tabela usuarios
. Para realizar uma consulta no banco, é utilizada uma função fictícia chamada querySql
.
function verificarSenha(usuario, senha) {
return querySql(
`SELECT * FROM usuarios WHERE usuario = '${usuario}' AND senha = '${senha}';`
);
}
Nesse exemplo, se alguém tentar acessar o sistema com o usuário admin
e senha 1234
, a query será:
SELECT * FROM usuarios WHERE usuario = 'admin' AND senha = '1234';
Essa é uma consulta "normal". O problema é que, como não é feito nenhum tratamento sobre os dados que o usuário passou para a função, concatenando-os diretamente na consulta SQL final, esse código pode sofrer de "injeção de SQL".
Um atacante pode informar para o sistema o usuário admin' --
e a senha 1
. Isso parece não fazer sentido, mas considerando a concatenação feita na função verificarSenha
, o resultado seria:
SELECT * FROM usuarios WHERE usuario = 'admin' --' AND senha = '1';
O resultado final é uma busca pelo usuário admin
, sem nenhuma outra condição, porque --
é o início de um comentário em SQL, então todo o resto é ignorado. Este é um exemplo simples que mostra que é possível ignorar a comparação inicialmente desejada pelo programador, e fazer uma comparação totalmente diferente.
Esse tipo de vulnerabilidade não se restringe ao SELECT
e também não ocorre apenas com comparações simples como exemplificado acima. Pode afetar comandos de modificação de dados como UPDATE
e DELETE
, ou até mesmo executar múltiplos comandos se a configuração do driver/banco permitir.
Exemplos reais de SQL Injection
Essa vulnerabilidade é muito antiga, mas é muito comum até hoje, mesmo sendo um problema com solução conhecida.
Para exemplificar o quão antiga ela é, em 2007 foi publicada a tirinha 327 do XKCD, com o famoso "Bobby tables":
Na tirinha, uma criança com o nome Robert'); DROP TABLE Students;--
foi cadastrada no sistema de uma escola, e isso fez com que perdessem os dados de todos os alunos. Pode ler uma explicação completa no site Explain XKCD.
Apesar dessa tirinha ter caráter cômico e parecer exagerada, existem vários casos reais de SQL Injection:
- Ataque à loja
guess.com
em 2002, com o vazamento de nome, número e data de validade de mais de 200.000 cartões de crédito. Fonte. - Ataque ao The Pirate Bay em 2010, com o vazamento de dados de usuário incluindo IPs e senhas em MD5. Fonte.
- Ataque à Sony em 2011, com o vazamento de senhas de um milhão de usuários. Fonte.
- Ataque ao FlyCASS em 2024, conseguindo autorização para não passar pela triagem de segurança de aeroportos. Fonte.
Pode ver outros exemplos no Wikipedia.
Quando eu usava o Stack Overflow ativamente, via com frequência comentários apontando problemas de injeção de SQL em perguntas, e às vezes até mesmo em respostas. Agora com as LLMs, onde não há terceiros para apontarem o problema, não sei se vamos nos deparar com uma alta de vulnerabilidades assim novamente.
Como resolver SQL Injection?
É comum as bibliotecas que realizam a consulta ao banco de dados aceitarem os parâmetros separados da consulta SQL. A função verificarSenha
do início da publicação ficaria parecida com:
function verificarSenha(usuario, senha) {
return querySql(
`SELECT * FROM usuarios WHERE usuario = $1 AND senha = $2;`,
[usuario, senha]
);
}
Geralmente as pessoas descobrem que isso é o suficiente para resolver o problema de SQL Injection e param por aqui, porque a documentação da biblioteca (ou um colega) diz que é o suficiente para prevenir esse problema.
Para aprofundar o conhecimento e entender como isso funciona é preciso entender o que a biblioteca que você está usando faz, e também como o banco de dados realiza a consulta.
Biblioteca pg
(PostgreSQL)
A motivação para pesquisar mais sobre o assunto e escrever este conteúdo surgiu de um comentário do gabrielmarques no curso.dev (se você é aluno, pode ver o comentário aqui). Nesse comentário, que aprofundou ainda mais o assunto tratado na aula, o gabrielmarques
ressaltou o funcionamento da biblioteca pg
(node-postgres), que é usada no TabNews e também no curso.dev, e como ela interage com o PostgreSQL.
Conforme a documentação da biblioteca, a consulta SQL e os valores dos parâmetros são passados separadamente para o servidor PostgreSQL. É o servidor PostgreSQL que lida com a combinação segura desses dois elementos, sem que a biblioteca pg
precise fazer sanitização ou escape manual dos parâmetros.
Para prevenir SQL Injection, o PostgreSQL usa um mecanismo robusto, geralmente por meio de "Prepared Statements" (declarações preparadas) ou um fluxo de protocolo muito similar ("Extended Query Protocol"). O processo funciona em etapas:
- Preparação (Parse): A instrução SQL com os placeholders (
SELECT * FROM usuarios WHERE usuario = $1 AND senha = $2;
) é enviada primeiro ao servidor. O PostgreSQL analisa essa estrutura (faz o parse), verifica a sintaxe e cria um plano de execução genérico. Nesta fase, a "intenção" da query é compreendida e fixada, antes mesmo de o servidor conhecer os valores dos parâmetros, mas pode saber os tipos de dados deles. - Vínculo (Bind): Os valores dos parâmetros (
'admin '' --'
e'1'
) são enviados separadamente. - Execução (Execute): O PostgreSQL então combina o plano de execução pré-compilado com os valores dos parâmetros fornecidos. Esses valores são tratados estritamente como dados, e não como parte do código SQL. Eles preenchem os "espaços" definidos no plano, não tendo a chance de alterar a estrutura do comando SQL.
Por exemplo, se um parâmetro $1
recebe o valor 'admin' --
, o PostgreSQL não vai interpretar --
como um início de comentário dentro da lógica da query. Ele vai procurar por um usuario
cujo valor seja literalmente a string admin' --
.
Para avaliar o plano da query de exemplo com o uso de parâmetros, executei o SQL abaixo diretamente para simular o que a biblioteca pg
faria nos bastidores:
PREPARE plano(TEXT, TEXT) AS
SELECT * FROM usuarios WHERE usuario=$1 AND senha=$2;
EXPLAIN EXECUTE plano('admin '' --', '1');
O uso de duas aspas simples seguidas ''
foi para escapar a aspas simples e conseguir simular a entrada admin ' --
.
O plano de execução gerado foi:
Index Scan using usuarios_usuario_key on usuarios (cost=0.14..8.16 rows=1 width=829)
Index Cond: ((usuario)::text = 'admin '' --'::text)
Filter: ((senha)::text = '1'::text)
Repare que tudo do primeiro parâmetro $1
(o valor 'admin '' --'
) foi interpretado como um literal de texto (::text
), e a tentativa de injeção de SQL não alterou a estrutura do plano de execução, que continua sendo um Index Scan
seguido de um Filter
.
Problemas de segundo nível
Mesmo que você tenha resolvido essa vulnerabilidade no seu sistema, outros sistemas podem depender do seu e não estarem preparados para isso. Quer um exemplo? Este comentário no Hacker News, de 2023, cita algumas empresas com nomes "peculiares" no site do governo do Reino Unido.
Uma dessas empresas se chama ; DROP TABLE "COMPANIES";-- LTD
(veja pelo Web Archive).
Mas, se for visitar a página hoje, o nome está: [NAME AVAILABLE ON REQUEST FROM COMPANIES HOUSE]
.
Provavelmente isso não era um problema para o site, visto que funcionou por anos exibindo o nome "com SQL Injection", mas poderia ser um problema para os sistemas que consumiam os dados das empresas listadas para salvar nos próprios bancos de dados. Infelizmente não encontrei uma fonte explicando se este realmente foi o motivo da mudança.
Tem outro exemplo que comentaram no curso.dev, onde tentaram resolver a injeção de SQL impedindo a entrada de palavras específicas, e por causa disso o cliente não conseguia cadastrar uma cidade chamada Alter do Chão (a palavra ALTER
não era permitida).
Então, dependendo de como os dados do seu sistema são consumidos, vale a pena deixar documentado que esse tipo de situação pode acontecer, e que os sistemas terceiros que consumirão os dados devem estar preparados para cenários assim.
Vulnerabilidades similares
É preciso tomar cuidado com qualquer tipo de "injeção". Por exemplo, se você tem um sistema onde o conteúdo gerado pelo usuário pode ser interpretado como HTML, como acontece no TabNews, precisará tomar cuidado com código JavaScript, ou HTMLs "exagerados" (muitos elementos, cores, animações etc.).
Outro exemplo de problema similar e real foi a injeção de CSS pelo MathJax descoberta no GitHub em 2024, permitindo customizar perfis como na imagem abaixo ou como outro exemplo que pode ser visto no Web Archive.
Até mesmo a sua base de código pode estar vulnerável a backdoors por causa de caracteres inesperados. Você pode ver aqui um exemplo de backdoor em JavaScript com um "caractere invisível". Atualmente o VS Code destaca caracteres assim.
Todos os problemas acima, incluindo o SQL Injection, envolvem "entradas de usuário". Como pode ver, isso varia desde o exemplo mais simples que dei no início da publicação, até casos de backdoor. Mas, mesmo os casos mais simples são graves.
Então, quando usar entradas de usuário, valide-as, sanitize-as (quando apropriado e com cuidado), e principalmente, utilize mecanismos de escape e parametrização específicos para o contexto (como prepared statements para SQL, ou funções de escape de HTML para saídas web). Não tente resolver isso você mesmo com um Regex enorme e complexo. Do contrário, pode ser que encontre mais uma vulnerabilidade, o ReDoS.
Para se aprofundar mais sobre opções de defesa contra a injeção de SQL, tem um excelente artigo em OWASP SQL Injection Prevention Cheat Sheet, abordando pontos que não falei aqui, como nível de privilégio do usuário do banco, e com exemplos em outras linguagens de programação.
Para entender melhor o caso específico do projeto em que você está trabalhando, leia a documentação do banco de dados, do driver/pacote usado para acessar o banco, e também do ORM, caso esteja utilizando um.
Fonte: https://curso.dev