Bem vindos de volta, vamos agora para nosso terceiro post sobre desenvolvimento seguro. Espero que esse conteúdo esteja agregando a cada um de maneira significativa, lembrando que, quaisquer dúvidas que surgirem, correções, debates, serão bem recebidos. Agora falaremos de:
Lidando com Inputs de Usuários
A maioria dos problemas de segurança em aplicações podem ser traçados a partir de como as entradas dos usuários é manipulada. Inputs de usuário não ocorrem apenas em campos ou formulários, mas qualquer coisa que um usuário envia para um servidor web ou aplicação e que cruza um limite de segurança.
Ataques de injeção
A vulnerabilidade de injeção existe quando dados manipulados são interpretados como uma instrução por um sistema sem a intenção de que isso ocorra. Uma das formas mais conhecidas desses ataques são:
Injeção de SQL (SQL Injection)
Considere o seguinte código dentro de um handler de um formulário:
Query = “SELECT * FROM pessoas WHERE primeironome = ‘$post.primeironome’”;
Connection.execute(Query);
Nesse exemplo o programa lê o valor de uma requisição POST, substitui ele por “$post.primeironome e então executa a string resultante.
Quando um atacante digita nesse campo:
Joao’; DROP TABLE pessoas; --
O que seria executado é:
Select * FROM pessoas WHERE primeironome = ‘Joao’; DROP TABLE pessoas;--
A causa raiz de ataques de injeção é que os dados são interpretados como instrução SQL.
O exemplo acima pode nem sempre funcionar, mas injeções de SQL mais sutis são possíveis. O exemplo a seguir mostra como fazer login numa aplicação web sem mesmo saber a senha correta:
Query = “SELECT * FROM usuários WHERE username = ‘$post.username’ AND password =’$post.password’”;
Se um atacante informar o nome de usuário seguinte:
Admin’ OR ‘a’ = ‘b’
E senha ‘xyz’, o seguinte código seria executado:
SELECT * FROM usuários WHERE username = ‘Admin’ OR ‘a’=’b’ AND password = ‘xyz’;
Como a = b é uma condição falsa e “AND” é um operador que tem maior precedência do que OR, isso é efetivamente o mesmo que:
SELECT * FROM usuários WHERE username = ‘Admin’
A verificação da senha que foi implementada na query é neutralizada através de uma injeção SQL e o atacante conseguiria obter o login sem conhecer a senha. Um outro exemplo já falado anteriormente é de limitar privilégios dos usuários de banco que realizam essas consultas. Um usuário que faz SELECT para trazer resultados, não precisa ter permissão para DROP para excluir uma tabela.
Queries diretas e Queries parametrizadas
Vimos que uma das causas de injeção de SQL é que o input do usuário é substituído antes que seja interpretado como SQL. Se interpretarmos o SQL e somente depois substituir o input do usuário, não haveria possibilidade de enganar o interpretador SQL.
Um mecanismo chamado de queries parametrizadas faz exatamente isso:
- Primeiro, uma declaração SQL que contém instruções reservadas é entregue à rotina do banco, onde ela é interpretada.
- Então o input do usuário é substituído por essas instruções reservadas.
As formas não parametrizadas de SQL são chamadas de queries diretas.
Queries parametrizadas são chamadas também de “prepared statements”. Exemplo:
Query = connection.prepare(“SELECT * FROM pessoas WHERE primeironome = ?”)
Connection.execute(Query, $post.primeironome)
A função prepare irá interpretar a query. Depois de interpretada, o valor de $post.primeironome será colocado no parâmetro “?”. Devido ao fato de nenhuma interpretação adicionar ser feita, não é possível alterar o comando que será executado como no exemplo anterior:
Joao’; DROP TABLE pessoas; --
O banco irá procurar por todos os registros que primeironome for igual à “Joao’; DROP TABLE pessoas; –“.
Validação de entrada
Uma técnica bastante utilizada é a de “escaping” que consiste em neutralizar meta-caracteres como “, ‘, &, <,>, neutralizar esses caracteres tornará muito difícil fazer com que um formulário seja interpretado como uma instrução SQL.
Validação de entrada parte do princípio de que “se sabemos exatamente o que esperamos nesse campo, porque permitir algo além disso?”. Essa abordagem pode ser tomada de diferentes formas:
- Blacklist ou DenyList: Irá rejeitar tudo que é especificamente marcado como proibido.
- Whitelist ou AllowList: Irá apenas aceitar o que for marcado como permitido.
Isso pode parecer sutil, mas tem consequências importantes. Se um item for esquecido de colocado na whitelist a sua entrada será rejeitada, por outro lado se a blacklist não estiver completa poderá ocorrer entradas maliciosas. Entretanto, abordagens whitelist são mais seguras do que as blacklist.
Nem sempre podemos nos dar o luxo de aplicar um filtro de whitelist, alguns meta-caracteres precisam ser aceitos, como por exemplo no nome “D’Villa”.
Enconding de entradas e validação
Supondo que recebemos uma entrada no navegador que contém caracteres que não podem ser transmitidos como estão. Uma solução seria em codificar esse dado, por exemplo usando BASE64. Nosso servidor receberia a string e poderíamos validar diretamente essa string, mas isso não ajudaria muito, afinal, ela ainda contém inputs maliciosos. Uma abordagem interessante é que devemos primeiro decodificar e depois validar.
Algumas vezes existem mais formas de representar o mesmo valor, por exemplo, o caractere < em HTML pode ser escrito de diversas formas: < < <.
Ao invés de complicar a rotina de validação considerando as diferentes formas, podemos normalizar a string, para que uma representação única seja utilizada. Entretanto, devemos normalizar antes de validar e processar.
É importante dizer que se um sistema trabalhar com conjuntos de caracteres diferentes, pode haver interpretações erradas. O ideal é que todo o sistema use a mesma codificação (UTF-8 é uma boa escolha).
Tamanho da entrada
Até agora olhamos para entradas maliciosas, mas algumas vezes o tamanho dessa entrada pode gerar um problema de segurança. Isso tem a ver com a alocação de memória e com a forma pela qual linguagens como C ou C++ podem livremente acessar a memória. Se o dado pode ser escrito fora do range de memória especificado, um atacante pode conseguir modificar o estado interno do programa. Isso é geralmente conhecido como Buffer Overflow.
Linguagens que não usam ponteiros para manipulação de memória NÃO estão imunes a esse tipo de ataque. Geralmente, os interpretadores são escritos em linguagens como C ou C++ e algumas extensões para as linguagens e bibliotecas adicionais são escritas em C ou C++.
Outras formas de injeção
Alguns tipos de injeção podem ser muito difíceis de encontrar, existem diversas outras formas, como por exemplo o Cross Site Scripting, em que um atacante consegue injetar um javascript malicioso e realizar um grande estrago dependendo do contexto da aplicação. Pode haver casos de informação que é armazenada no sistema e só depois será processada por um outro sistema completamente diferente, como por exemplo, uma aplicação que gera logs que podem ser acessados por uma outra aplicação administrador que por sua vez é vulnerável a ataques de injeção. Encontrar essas brechas é extremamente desafiador principalmente se um sistema é desenvolvido por vários times.
Leitura complementar
https://www.welivesecurity.com/br/2017/07/07/vulnerabilidade-cross-site-scripting/