Corrigindo a injeção de SQL: ORM não basta
8 de junho de 2016
0 minutos de leituraUm dos tipos de vulnerabilidade mais perigosos e disseminados é a injeção de SQL, que dá aos invasores acesso ao seu banco de dados de back-end. Usar prepared statements e mapeamento objeto-relacional (ORM) é uma boa forma de se proteger contra injeção de SQL, mas não basta. Como mostramos neste post, pacotes de ORM como Sequelize e MySQL podem apresentar — e de fato apresentam — falhas que deixam você vulnerável. Para se proteger de verdade, é preciso fazer mais.
No nosso Relatório sobre o Estado da Segurança do Open Source de 2019, descobrimos que as vulnerabilidades de injeção de SQL ainda são uma causa comum de preocupação com segurança, com um pico de 16 vulnerabilidades encontradas em bibliotecas no repositório PHP Packagist.
Resumo
Uma vulnerabilidade de injeção de SQL, ou SQLi, permite que um invasor adicione — ou “injete” — texto não estruturado em um comando SQL, causando consequências inesperadas. Uma forma de se proteger contra injeção de SQL é usar um pacote de ORM, que converte seus objetos e as ações realizadas sobre eles em SQL. Com esses pacotes, você não escreve SQL diretamente e, portanto, não corre o risco de cometer um deslize e permitir injeções maliciosas. Viva!
Mas é fácil esquecer que, só porque você não está escrevendo SQL, não significa que ele não esteja sendo escrito. De fato, os pacotes de ORM no npm ainda precisam converter essas ações em SQL. Esses pacotes são softwares, como todos os outros, e softwares podem ter bugs. No último ano, foram reportadas 4 vulnerabilidades de injeção de SQL nos dois principais pacotes de ORM do npm, sequelize e node-mysql, transformando essa preocupação teórica em realidade. Esses problemas foram corrigidos nas versões mais recentes dos pacotes, mas você pode estar usando versões antigas, e novas vulnerabilidades podem surgir.
Este post apresenta alguns exemplos dessas falhas e explica quais outras camadas de defesa você deve adotar. As principais são:
Teste suas dependências para encontrar vulnerabilidades
Valide as entradas, mesmo ao usar ORM
Antes de nos aprofundarmos nos cenários problemáticos, vamos relembrar rapidamente o que é injeção de SQL, quais são as opções para se proteger contra ela e o que é ORM.
Exemplo simples de injeção de SQL
Imagine um aplicativo de lista de tarefas com uma função de busca por itens que contenham um determinado texto. A busca é feita por uma consulta ao banco de dados, implementada com o pacote sequelize. Supondo que a conexão com o banco de dados já esteja configurada, esta é a função de busca no back-end:
Em um uso legítimo, o usuário digita um valor simples (por exemplo, Buy), que gera a seguinte consulta legítima e retorna todas as tarefas de compras relevantes da nossa lista:
No entanto, um invasor pode enviar intencionalmente um caractere de aspas simples (') para sair do contexto de string previsto e interferir na própria consulta. Por exemplo, ele pode inserir o valor ') UNION SELECT username||'_'||password FROM Users --, gerando a seguinte consulta:
Supondo que tenhamos uma tabela Users com os campos password e username, a consulta acima adicionará à nossa lista de tarefas TODO todos os nomes de usuário e senhas. O restante do texto é facilmente transformado em comentário usando o comando --, mantendo a consulta SQL válida.
Essa implementação vulnerável pode parecer especialmente ingênua, mas variações dela são bastante comuns. Em todos os casos, algum tipo de entrada do usuário sem validação é acrescentado a uma consulta SQL bruta, saindo do contexto original (por exemplo, uma string) e acionando ações não previstas. Por outro lado, o ataque que mostramos era relativamente simples e direto, mas invasores reais enviam gradualmente muitas variações de ataques e precisam que apenas uma delas funcione.
Impacto e correção
A injeção de SQL é uma vulnerabilidade extremamente grave. Na maioria dos casos, uma única injeção de SQL em qualquer parte do seu site pode acabar permitindo a execução de qualquer consulta no banco de dados, além da extração e manipulação dos dados. Como os bancos de dados costumam guardar as informações mais sensíveis do sistema, permitir esse tipo de acesso a invasores é devastador.
Há duas técnicas principais — que podem ser usadas em conjunto — para evitar a injeção de SQL: validação de entrada e prepared statements.
Validação de entrada
Assim como outros ataques de injeção, a injeção de SQL começa com uma entrada maliciosa do usuário. Por isso, uma boa forma de evitá-la é verificar se a entrada fornecida pelo usuário é válida. Se não for, podemos interromper a operação ou remover os caracteres potencialmente perigosos.
A validação de entrada pode ser feita com um modelo de segurança negativa ou positiva.
O modelo de segurança negativa consiste em bloquear caracteres ou padrões perigosos específicos. No exemplo acima, por exemplo, poderíamos bloquear a aspa simples que permitiu sair do contexto de string. Infelizmente, SQL é uma linguagem complexa, o que dificulta identificar todos os caracteres potencialmente perigosos. Nesse mesmo exemplo, também precisaríamos bloquear caracteres como backspace (b), barra invertida (\), nulo (x00) e provavelmente vários outros — e a lista aumenta se houver suporte a vários tipos de banco de dados. Ainda assim, bloquear caracteres perigosos é uma técnica de mitigação relativamente simples e eficaz.
O modelo de segurança positiva permite apenas caracteres específicos. No exemplo acima, limitar a entrada a letras, números e espaços (/a-zA-Z0-9 /) teria resolvido o risco de forma eficaz. Essa abordagem, também chamada de lista de permissões, costuma ser melhor do ponto de vista da segurança, pois evita surpresas causadas por valores que você não considerou. No entanto, ela pode bloquear caracteres legítimos, especialmente quando se amplia o suporte a conjuntos de caracteres Unicode.
Prepared statements e ORM
Se os ataques de injeção de SQL começam na entrada, terminam na consulta SQL — e é nela que temos uma segunda oportunidade de evitar o problema. Nesse ponto, a raiz do problema é a simples concatenação de strings usada para criar a consulta SQL. Se, em vez disso, usássemos um modelo para a consulta, poderíamos informar ao banco de dados (ou à biblioteca de conexão) que aquilo deve ser tratado como um valor de string, deixando que ele o codifique conforme necessário. Essa é a solução mencionada no início deste post.
Esses modelos são mais conhecidos como “prepared statements” ou, às vezes, “instruções parametrizadas”. O pacote sequelize que usamos acima também oferece suporte a eles. Portanto, poderíamos corrigir a vulnerabilidade modificando nossa função da seguinte forma:
O Sequelize entende que ? representa um valor, e não um comando SQL, e o codifica adequadamente, bloqueando tentativas de escapar do contexto.
Prepared statements não são apenas uma boa forma de proteger seu código: também o tornam mais legível e fácil de manter. Em outras palavras, sempre que sentir vontade de concatenar valores em uma instrução SQL, resista à tentação e use um prepared statement.
Indo um passo além, uma abordagem ainda mais programática ao SQL é o uso de mapeamento objeto-relacional (ORM). Usar ORM significa mapear as tabelas do banco de dados para seus objetos, permitindo ler, gravar e consultar objetos inteiros. Como o ORM reduz ainda mais o uso de SQL explícito, também é uma boa forma de evitar a injeção de SQL.
Quando os ORMs são vulneráveis: Sequelize
Prepared statements e ORM são boas formas de delegar a responsabilidade pela codificação aos “especialistas” — os pacotes especializados justamente nisso. Mas ser especialista não significa nunca ter bugs… Como mencionado no início, isso ficou evidente no último ano, com 4 vulnerabilidades de injeção de SQL em dois dos principais pacotes de ORM do npm, sequelize e node-mysql.
As vulnerabilidades decorreram, de forma consistente, de parâmetros não validados em várias chamadas de ORM e prepared statements. No fim das contas, essas chamadas ainda precisam converter os parâmetros em uma instrução SQL e podem deixar de escapá-los ou validá-los durante esse processo.
Vamos analisar algumas vulnerabilidades do sequelize para entender melhor o que aconteceu. Vale ressaltar que elas já foram corrigidas e que os desenvolvedores do Sequelize agiram rapidamente assim que os problemas foram encontrados e reportados. Você encontra a lista completa de vulnerabilidades do Sequelize no Banco de Dados de Vulnerabilidades da Snyk, junto com orientações para corrigi-las.
A primeira, divulgada em janeiro de 2016 e corrigida na versão 3.17.0, afetava a função findAll, frequentemente usada para consultar objetos no banco de dados com ORM. Ao converter os parâmetros para SQL, a função não restringia os valores do parâmetro LIMIT, abrindo caminho para uma vulnerabilidade de injeção de SQL.
Veja um exemplo de como essa vulnerabilidade poderia ser explorada em uma lista de tarefas TODO.
Supondo que Items contenha os campos Username e Desc, a consulta resultante seria mais ou menos assim:
Outra falha semelhante foi divulgada no fim de março de 2016. Dessa vez, o problema ocorria ao criar prepared statements e concatenar valores para a instrução IN. Veja um exemplo de ataque:
Quando identificadas, essas vulnerabilidades não são muito complexas, mas mostram que até pacotes populares podem falhar. SQL é uma linguagem complexa, e é fácil deixar passar casos extremos.
Solução: defesa em profundidade
Se ainda não ficou claro, vou dizer sem rodeios: você definitivamente deve usar ORM e prepared statements. Eles eliminam a maior parte do risco de injeção de SQL e, de modo geral, são boas práticas de desenvolvimento. No entanto, não pense que usar esses pacotes deixa você completamente imune.
Em vez disso, você também deve validar as entradas. Assim, as entradas maliciosas nem chegam ao seu sistema, o que ajuda muito a reduzir o risco. A aplicação de várias camadas de defesa como essa é conhecida como “defesa em profundidade” e é uma ótima prática. Se um ataque passar por uma camada de defesa (a validação de entrada), a segunda provavelmente o bloqueará. Se cada camada deixasse passar 1% dos ataques, duas camadas bloqueariam todos, exceto 0,01% deles — uma conta e tanto.
Além disso, você deve acompanhar as vulnerabilidades conhecidas nos pacotes que usa e corrigi-las assim que forem identificadas. Você pode usar a Snyk gratuitamente para testar seus aplicativos em busca das vulnerabilidades mencionadas e integrar testes de vulnerabilidade ao seu processo de desenvolvimento para ajudar a manter tudo protegido.
Adorado por desenvolvedores. Confiável para a segurança.
As ferramentas da Snyk, pensadas para desenvolvedores, oferecem segurança integrada e automatizada para atender às suas necessidades de governança e conformidade.
