Guia de prevenção contra ataques de injeção de SQL: 8 práticas recomendadas
26 de março de 2021
0 minutos de leituraA injeção de SQL é uma das vulnerabilidades mais perigosas em aplicações online. Ela ocorre quando alguém adiciona dados não confiáveis a uma consulta ao banco de dados, por exemplo, ao preencher um formulário web. Se a injeção de SQL for possível, invasores experientes podem criar entradas maliciosas para roubar dados valiosos, contornar a autenticação ou corromper os registros do banco de dados.
Existem diferentes tipos de ataques de injeção de SQL, mas, em geral, todos têm uma causa semelhante: os dados não confiáveis inseridos pelo usuário são concatenados à string da consulta. Assim, a entrada do usuário pode alterar a intenção original da consulta.
Veja alguns exemplos de injeção de SQL:
Adicionar uma condição booleana sempre verdadeira à cláusula WHERE, como
' OR 1=1Escapar parte da consulta inserindo comentários de linha
--Encerrar a consulta inicial e começar uma nova
'; DROP TABLE USERS;Combinar dados de várias tabelas usando
UNION
Neste guia, apresento oito práticas recomendadas que todo profissional de desenvolvimento pode adotar para evitar ataques de injeção de SQL. Vamos começar a proteger sua aplicação contra SQLi.

Não confie na validação de entrada no lado do cliente
Use um usuário de banco de dados com privilégios limitados
Use instruções preparadas e parâmetros nas consultas
Analise seu código em busca de vulnerabilidades de injeção de SQL
Use uma camada de ORM
Não confie em listas de bloqueio
Valide as entradas
Tenha cuidado com procedimentos armazenados
1. Não confie na validação de entrada no lado do cliente.
A validação de entrada no lado do cliente é muito útil. Com ela, você já consegue impedir que dados inválidos cheguem à lógica do sistema. No entanto, isso só funciona para quem não tem más intenções e quer usar o sistema como previsto. Dar um retorno direto ao usuário quando um valor não é válido é muito útil e melhora a experiência. Por isso, use a validação no lado do cliente para aprimorar a experiência do usuário.
Quando se trata de injeção de SQL, porém, não é um método em que você deva confiar. É possível remover a validação no lado do cliente alterando o código JavaScript carregado no navegador. Além disso, em uma arquitetura cliente-servidor, é muito fácil fazer uma chamada HTTP básica ao back-end com um parâmetro que provoque uma injeção de SQL — usando ferramentas como Postman ou comandos tradicionais do curl.
Você deve validar os dados no lado do servidor, de preferência o mais próximo possível da origem — neste caso, no momento em que a consulta SQL é criada. Tudo o que o cliente envia deve ser considerado potencialmente nocivo. Por isso, confiar na validação do lado do cliente para prevenir injeção de SQL é uma péssima ideia.
2. Use um usuário de banco de dados com privilégios limitados
Como mencionado, existem diferentes tipos de ataques de injeção de SQL, e alguns são mais nocivos que outros. Imagine que minha consulta SQL seja algo como "SELECT * FROM USER WHERE USERID = '" + userid +"'". A injeção " foo' OR '1'='1 " retornará todos os usuários e já será prejudicial. No entanto, " '; UPDATE message SET password = 'EVIL" causará ainda mais problemas, pois o invasor terá alterado todas as entradas.
Ao criar um usuário de banco de dados para sua aplicação, considere os privilégios que ele terá. A aplicação precisa ler, gravar e atualizar todos os bancos de dados? E quanto a esvaziar ou excluir tabelas? Ao limitar os privilégios da aplicação no banco de dados, você reduz o impacto de uma injeção de SQL. Provavelmente, é melhor não usar um único usuário de banco de dados para a aplicação. Crie vários usuários e associe-os a funções específicas da aplicação. Problemas de segurança costumam ser uma reação em cadeia. Para evitar danos graves, fique atento a todos os elos dessa cadeia.
3. Use instruções preparadas e parâmetros nas consultas
Muitas linguagens oferecem recursos integrados que ajudam a prevenir a injeção de SQL. Ao escrever consultas SQL, você pode usar uma instrução preparada para compilá-las. Com ela, é possível parametrizar as consultas. A parametrização é uma técnica para criar instruções SQL dinamicamente: você define a consulta base com alguns espaços reservados e associa a eles, com segurança, os parâmetros fornecidos pelo usuário.
Quando você usa uma instrução realmente preparada e consultas parametrizadas, o próprio banco de dados cuida do escape. Primeiro, ele cria o plano de execução da consulta com base na string que contém os espaços reservados. Depois, os parâmetros (não confiáveis) são enviados ao banco de dados. Como o plano da consulta já foi criado, os parâmetros não podem mais influenciá-lo. Isso impede a injeção.
Exemplo em Java:
Exemplo em Python com o conector MySQL:
Exemplo em JavaScript com mysql2:
Há várias maneiras de fazer isso em JavaScript, por exemplo, com um banco de dados MySQL. Atenção: ao usar .query(), você não executa uma instrução realmente preparada. Nesse caso, a substituição dos parâmetros é feita no lado do cliente, ou seja, você está simulando uma instrução preparada. Para executar uma instrução realmente preparada no banco de dados, use a função .execute().
4. Analise seu código em busca de vulnerabilidades de injeção de SQL
Criar código personalizado pode parecer fácil, mas é fácil cometer erros. Talvez você já tenha processos para revisar o código, como revisão de código e programação em pares. Mas será que quem revisa seu código ou programa com você entende de segurança? Essa pessoa consegue identificar um erro de injeção de SQL no seu código? De qualquer forma, seria bom analisar automaticamente o código personalizado em busca de possíveis vulnerabilidades de segurança, como injeção de SQL. Com uma ferramenta de SAST (teste estático de segurança de aplicações), como o Snyk Code, você pode inspecionar automaticamente seu código em busca de vulnerabilidades, como injeção de SQL. Isso pode ser automatizado facilmente no seu ciclo de vida de desenvolvimento de software, por exemplo, conectando seu repositório Git à Snyk.

5. Use uma camada de ORM
Você também pode considerar o uso de uma camada de mapeamento objeto-relacional (ORM). Uma camada de ORM transforma os dados do banco de dados em objetos e vice-versa. O uso de uma biblioteca ORM reduz a quantidade de consultas SQL explícitas e, portanto, a exposição à injeção de SQL.
Alguns exemplos de bibliotecas ORM conhecidas são Hibernate para Java e Entity Framework para C#. A tipagem forte dessas linguagens geralmente permite gerar o mapeamento entre objetos e tabelas do banco de dados. Assim, você nem precisa criar consultas SQL diretamente.
Ainda assim, o problema pode surgir quando você precisa criar consultas personalizadas. O Hibernate para Java tem sua própria linguagem de consulta, a Hibernate Query Language (HQL). Ao compilar consultas HQL, fique atento à possibilidade de injeção e use a função createQuery(), que funciona de forma semelhante a uma instrução preparada.
Também existem bibliotecas ORM conhecidas para JavaScript, como sequelize. Com o sequelize, você pode definir como os valores são mapeados para tipos específicos no banco de dados. Mas, na prática, uma biblioteca ORM precisa traduzir a lógica de volta para instruções SQL. Portanto, confiamos que essas bibliotecas implementem o escape correto dos parâmetros.
Para ter certeza de que sua biblioteca ORM não apresenta problemas de injeção de SQL, procure vulnerabilidades conhecidas. Usar uma versão desatualizada ou inadequada do sequelize ou do hibernate ainda pode causar problemas. Use o Snyk Open Source para analisar seu projeto e evitar vulnerabilidades ocultas de injeção de SQL nas bibliotecas, além de muitos outros problemas.
6. Não confie em listas de bloqueio
Acredito que muita gente já saiba disso, mas vou repetir: não implemente listas de bloqueio para seus parâmetros. Essa abordagem reúne regras que definem entradas vulneráveis. Se uma entrada corresponder a essas regras, a solicitação será bloqueada. No entanto, se as regras forem permissivas demais, uma entrada maliciosa ainda poderá passar. Se forem restritivas demais, uma entrada válida será bloqueada.
Por exemplo, podemos bloquear todas as solicitações que contenham a palavra OR. Pode parecer uma regra razoável, mas, na prática, “Or” é um nome israelense bastante comum. Isso significa que vários colegas seriam impedidos de inserir seus nomes. O mesmo vale para um apóstrofo '. Inúmeros nomes contêm esse caractere. Pense em O’Neill e O'Donnell, mas também em nomes como Dont’a, por exemplo.
7. Valide as entradas
Sim, você sempre deve validar as entradas! Embora instruções preparadas com consultas parametrizadas sejam a melhor defesa contra injeção de SQL, use sempre várias camadas de proteção. Assim como limitar os privilégios de um usuário de banco de dados, validar as entradas é uma ótima prática para reduzir os riscos gerais da sua aplicação. Além disso, há situações em que não é possível usar instruções preparadas. Algumas linguagens não oferecem suporte a esse mecanismo, e sistemas de banco de dados mais antigos podem não permitir que a entrada do usuário seja fornecida como parâmetro. Nesses casos, a validação de entradas é uma alternativa aceitável.
Como explicado anteriormente, a validação de entradas deve usar listas de permissão, e não listas de bloqueio. Defina uma regra que descreva todos os padrões permitidos, por exemplo, com uma expressão regular, ou use uma biblioteca bem mantida. Combine isso com instruções preparadas e consultas parametrizadas para obter uma defesa robusta.
8. Tenha cuidado com procedimentos armazenados
Muita gente acredita que usar procedimentos armazenados é uma boa forma de prevenir a injeção de SQL. Nem sempre é o caso. Assim como as consultas SQL criadas na aplicação, um procedimento armazenado também pode ser alvo de injeção maliciosa.
Assim como nas consultas SQL da sua aplicação, use parâmetros nas consultas do procedimento armazenado, em vez de concatenar os parâmetros. É muito fácil evitar a injeção de SQL em um procedimento armazenado.
Então, não faça isto no MySQL:
Em vez disso, use consultas parametrizadas nos procedimentos armazenados:
Como mostrado acima, as mesmas regras que se aplicam ao código da sua aplicação também valem para os procedimentos armazenados. A implementação desses procedimentos varia entre bancos de dados. Saiba como implementá-los no seu banco e fique atento à possibilidade de injeção. Embora eu acredite que seja melhor manter toda a lógica na aplicação, um procedimento armazenado pode ser uma solução razoável quando a linguagem usada no desenvolvimento não oferece suporte a instruções preparadas.


