Vulnerabilidades em aplicações Web: SQL Injection

Segurança de aplicações web deve sempre ser uma preocupação por parte de toda equipe de desenvolvimento de um sistema. Comumente vemos notícias de vazamento de dados em sistemas, brechas de segurança que permitem acesso a contas de outros usuários ou até mesmo brechas que permitem exclusão de toda a base de dados.

Falaremos nesse post sobre uma falha na codificação de sistemas conhecida como SQL Injection, uma das brechas mais comuns e também das mais fáceis de se evitar na construção de um software.

SQL Injection seria a injeção de instruções SQL através de inputs na aplicação vulnerável com o intuito de manipular os dados da aplicação. Nesse tipo de ataque, pode ocorrer acesso não autorizado a funcionalidades do sistema, vazamento de toda a base de dados ou até mesmo a remoção de toda essa base de informação!

Qual é a brecha?

A brecha acontece quando o programador descuidado concatena dados de input do usuário com a consulta SQL feita no banco de dados, sem realizar o escape da string passada pelo usuário, ocasionando a possibilidade de manipulação de instruções SQL diferentes da prevista pela aplicação.

Exemplo de aplicação vulnerável

Para mostrar como funciona na prática, criamos uma aplicação para testes de injeção de SQL, com uso de Java e Hibernate, disponível aqui.
alt Essa aplicação possui um exemplo seguro, a esquerda, e o exemplo com a brecha de segurança, a direita.
Temos os seguintes usuários em nossa base de dados:

Usuário Senha
Harryson C. Guimarães 42
João Grilo 123456
Logan X-23
Frank Underwood 2016-2020

Experimente entrar, em ambos os painéis da aplicação, com os usuários e senhas acima. Veja que o login dos usuários ocorre conforme esperado em ambos.

Contudo, no painel com a falha, temos a seguinte consulta sendo executada para verificação da existência do usuário no banco de dados:

String sql = "SELECT * FROM portal.usuario u WHERE u.nome = '" + usuario + 

"' and u.senha = '" + senha + "'";

Query query = JPA.em().createNativeQuery(sql, Usuario.class);

return query.getResultList();  

Somente para facilitar o entendimento da brecha, a lógica de login retorna uma lista de usuários. Numa aplicação real, o retorno deveria ser somente um usuário.

Essa consulta está aberta a injeção de SQL. Note no trecho acima os dados do usuário sendo concatenados com a consulta. Imagine se no campo de usuário digitarmos qualquer valor, como usuario_inexistente e no campo de senha inserirmos somente ' (aspas simples). Faça o teste! Veja que ocorre um erro, exibido para o usuário, e nesse erro temos uma pista do que está acontecendo. A consulta SQL gerada foi:

SELECT * FROM portal.usuario u WHERE u.nome = 'usuario_inexistente' and u.senha = ''';  

Veja que, como informamos uma aspas simples no campo de senha, a consulta SQL quebrou, pois ao concatenar o input do usuário com a consulta, essa consulta identificou a aspas simples como sendo o final da SQL e a aspas simples da própria SQL ficou sobrando, quebrando a instrução.

Faça um novo teste. No campo de senha, insira senha_inexistente' or 'a' = 'a (assim mesmo, sem fechar a última aspas). Agora a brincadeira ficou interessante! Veja que os dados de todos os usuários são exibidos. Como isso aconteceu? O que houve foi que a string informada no campo de senha, concatenada na consulta, gerou a seguinte SQL:

SELECT * FROM portal.usuario u WHERE u.nome = 'usuario_inexistente' 

and u.senha = 'senha_inexistente' or 'a' = 'a';  

Não temos um usuário chamado usuario_inexistente, nem mesmo alguém com a senha senha_inexistente. Porém, a condição or 'a' = 'a' torna toda a sentença verdadeira e nos traz a lista de todos os usuários do banco de dados. Através dessa falha, podemos explorar não só a tabela de usuários, mas todo o banco de dados, e até mesmo realizar um DROP e apagar todo o banco.

Como evitar?

Uma forma simples de evitar SQL Injection consiste em não concatenar diretamente o input do usuário com a consulta SQL. No modelo seguro de nossa aplicação, a consulta foi feita usando named parameters. Assim, os dados do usuário não são concatenados diretamente na consulta:

String jpql = "SELECT u FROM " + Usuario.class.getCanonicalName() + " u" +  
" WHERE u.nome = :usuario and u.senha = :senha";

Query query = JPA.em().createQuery(jpql, Usuario.class);

query.setParameter("usuario", usuario).setParameter("senha", senha);

return query.getResultList();  

Tente executar a invasão no painel seguro de nossa aplicação e veja que o erro não acontece.

Verificação automatizada com Sqlmap

Uma vez encontrada a brecha de segurança, muitas vezes não é trivial montar consultas quebrando a consulta original e concatenando novas instruções SQL. Para isso, existe uma ferramenta que automatiza o processo de verificação dessas brechas, chamada SqlMap, que é uma ferramenta open-source escrita em python e tem seu código-fonte hospedado no Github.

Através do Sqlmap direcionado para uma aplicação vulnerável, é possível fazer a verificação e ter acesso a todas as tabelas do banco, o nome do banco, nome das tabelas, visualizar tanto a estrutura das tabelas como realizar um dump desses dados.

Atenção: O uso dessa ferramenta é recomendado com o único intuito de verificação de brechas em sua própria aplicação. Não recomendamos que se tente usá-la para invadir sistemas alheios. Isso é crime, previsto na Lei 12.737/2012 no Art. 154-A:

Invadir dispositivo informático alheio, conectado ou não à rede de computadores, mediante violação indevida de mecanismo de segurança e com o fim de obter, adulterar ou destruir dados ou informações sem autorização expressa ou tácita do titular do dispositivo ou instalar vulnerabilidades para obter vantagem ilícita:

Pena – detenção, de 3 (três) meses a 1 (um) ano, e multa.

Fica a dica

Você desenvolvedor, NUNCA, em hipótese alguma, considere a opção de concatenar input do usuário com uma consulta no banco de dados. ;)