Se você acompanha nossas redes sociais, é bem provável que tenha se deparado com algum “Desafio de Codificação Segura” com o objetivo de identificar uma vulnerabilidade em algum código. Sabendo disso, separamos neste artigo um passo a passo que pode te ajudar a construir um método para conseguir resolver algum desses desafios, além de agregar conhecimento para quando realizar o Code Review da sua aplicação. Neste artigo, vamos trazer exemplos de código em Ruby on Rails.
Você também pode ouvir esse conteúdo:
Conforme o Guia de Code Review da OWASP, o Code Review é um um processo crucial no ciclo de desenvolvimento seguro, sendo uma das melhores maneiras de identificar vulnerabilidades. Realizado por times de desenvolvimento, ele pode ajudar a diminuir as possíveis explorações e vulnerabilidades no código logo no início do ciclo, contribuindo com a abordagem Shift-Left do DevSecOps.
Nesse sentido, desafios de segurança em código ajudam desenvolvedores a praticar habilidades de resolução de problemas, entender melhor a linguagem de programação que usam e a conhecer o funcionamento de ataques e vulnerabilidades.
Vamos lá, para facilitar o aprendizado, utilizaremos o seguinte exemplo de código vulnerável em Ruby on Rails:
1º passo: conheça a linguagem ou framework utilizado
1.1 Conheça a OWASP TOP 10 e as principais categorias de risco e suas vulnerabilidades
Antes de começar a revisar o código, é bom pensar quais são as vulnerabilidades mais comuns para esse tipo de linguagem ou framework, além de pensar na própria lista da OWASP TOP 10, com os principais riscos de segurança.
Familiarizar-se com os tipos de vulnerabilidades mais comuns o ajudará a identificar padrões semelhantes no código-fonte. Isto é, conhecer a linguagem de programação o ajudará a identificar padrões vulneráveis com mais precisão.
1.2 Consulte a documentação da linguagem
Neste caso, estamos utilizando Ruby on Rails. Como o próprio documento da linguagem explica, Rails é um framework full-stack, sendo utilizado em aplicações web no front e no back-end. Portanto, já podemos considerar problemas de segurança em aplicações web para nosso exercício.
1.3 Consulte tópicos sobre segurança na documentação da linguagem
Depois, consultamos a documentação da linguagem no que tange ao assunto de segurança. Encontramos que não há um padrão comum de vulnerabilidades por ser uma linguagem com uma comunidade muito ativa na resolução de problemas.
No entanto, a documentação chama a atenção para alguns tipos de ataques comuns para o framework: Session Hijacking, Cross-Site Request Forgery (CSRF), Injection.
Vamos nos atentar a esses tipos de ataques e quais tipos de vulnerabilidades são brechas para que aconteçam, mas não nos limitemos a eles. Aqui começamos e pensar no método dedutivo, estabelecendo uma premissa maior e criando relações com uma segunda proposição, chamada de premissa menor.
2º passo: entenda o propósito do código
Dependendo do nível de conhecimento que você tenha dos padrões de ataque, talvez já com as informações anteriores consiga encontrar a vulnerabilidade do código apresentado via dedução. Porém, como o foco desse texto é para desenvolvedores iniciantes nesse assunto, vamos pensar em detalhes.
2.1 Analise o fluxo de dados do código
Existem alguns conceitos importantes para se ter em mente durante a leitura do código e pensar na visão de um atacante, são eles: sources, sinks, e data flow.
Sources, como fonte, pode ser uma função que recebe a entrada do usuário. Sink representa funções que executam comandos do sistema. Depois de identificar o sources, precisamos entender o que o código faria com esses dados vindos do client.
Com isso em mente, se o input de um agente mal intencionado puder ir de source para sink em um trajeto conhecido como data flow, ou “fluxo de dados”, sem filtro, validação adequada ou sanitização, logo, esse fluxo pode representar uma vulnerabilidade de Injection.
Em suma, ao fazer essa análise, identifique onde os dados finalmente terminam. Entender, portanto, o funcionamento e a construção do fluxo de dados no código é essencial para identificar algumas das vulnerabilidades mais comuns.
Vamos utilizar o conceito na prática com o nosso caso:

Em Ruby, um módulo é uma coleção de métodos, variáveis e constantes armazenados em uma espécie de container. É semelhante a uma classe, mas não pode ser instanciada.
Resolvers são funções que o servidor GraphQL usa para buscar os dados para uma consulta específica. Cada campo de seus tipos GraphQL precisa de uma função de resolução correspondente. Quando uma consulta chega ao back-end, o servidor chamará as funções de resolução que correspondem aos campos especificados na consulta.
Encontramos aqui um fluxo de dados no código com a classe Bookings! Essa classe é construída através da interface ActiveRecord, biblioteca ORM que define o modo como os dados serão mapeados entre os ambientes, como serão acessados e gravados. Esse tipo de informação sobre o código, você consegue obter através da pesquisa da sintaxe da linguagem na sua documentação e fóruns de comunidades.

Dentro da classe, é definido o método resolve tendo como parâmetro as variáveis ligadas ao banco de dados via ORM, no qual por padrão todos inteiros recebem valor nulo (nil) com exceção da string order que fica em aberto via ‘ ‘ .

Pela padronização da forma como é construída a query no Return é possível identificar o padrão SQL.
⚠️ A funcionalidade de ordenação em banco de dados SQL (ORDER BY) é um ponto de atenção, pois é responsável por selecionar uma coluna ou um número e ordenar o resultado de acordo com os valores contidos nesta coluna. Como o argumento order em questão está conectado com o servidor GraphQL, é importante ficar atento a forma como essa conexão será construída.
No meio desse fluxo, ele define algumas condições lógicas importantes para entender o retorno desse método que vamos ver no próximo subtópico.
2.2 Analise o fluxo de controle do código
Com a ideia do fluxo de dados em vista, outra perspectiva que pode te ajudar a identificar problemas no código é analisar condições lógicas. Isso significa examinar uma função e identificar várias condições de ramificação, como loops, instruções if, blocos try/catch etc.
Entendendo a parte lógica do trecho, é possível descobrir sob quais circunstâncias e condições cada ramificação é executada.
Na prática com o nosso exemplo:

Os argumentos utilizados como parâmetros do método resolve são, via operador splat (*), enviados para cada função como um parâmetro individual.
⚠️ De modo geral, no campo order é definido um valor ao objeto na condição dele estar vazio, mas atenção, seu valor de recebimento em aberto logo na definição do método pode trazer complicações aqui caso o valor de input seja manipulado.
Em bookings há uma relação com a definição do usuário de acesso sem aplicar condições específicas, mas aplicando relações com a localização no banco de dados via where. Depois, obtemos o que o método nos retorna:

Ao ler o código em Ruby várias vezes, fica nítido que seu propósito é o de realizar consultas ordenadas. Agora, será possível mudar esse propósito? Vamos para o último passo: para a construção de hipóteses.
3º passo: faça testes de hipóteses com uma mentalidade hacker
Agora que você conhece de fato qual o objetivo do código, busque pensar “fora da caixa” e levante possíveis hipóteses, subvertendo o propósito original do código. Falamos aqui numa mentalidade hacker, no sentido de olhar as dinâmicas e processos de forma diferente do convencional.
3.1 Seja pessimista e imagine o pior
Com o propósito do código em mente, reflita:
- Qual é o pior cenário possível?
- Qual a probabilidade disso acontecer?
- Como o código pode gerar esse cenário?
“Pensar como um hacker” exige que você analise os sistemas de forma diferente, isso significa observar o óbvio, entender a possibilidade de erros humanos e a realizar uma espécie de abordagem de cadeia de eliminação de hipóteses para alcançar seu objetivo.
3.2 Subverta a ordem por meio de hipóteses
Agora que tem em perspectiva os piores cenários, faça uma reflexão sobre o código e construa hipóteses através de questões como:
- Eu consigo alterar a entrada dada e a saída esperada?
- Existe alguma validação para o dado de entrada? Se sim, é possível subvertê-la?
- Existem restrições ou limitações para a entrada?
- É possível alterar o tipo de dados da entrada fornecida?
- O programa transfere dados confidenciais sem criptografia?
- O método de criptografia é suscetível a ataques?
- Se o programa usa strings aleatórias, elas são aleatórias o suficiente?
E assim por diante.
Voltamos ao nosso exercício inicial. Até aqui, conseguimos identificar várias informações importantes sobre o código.
Utiliza Ruby on Rails, sendo um dos principais ataques para aplicações nessa linguagem: Session Hijacking, Cross-Site Request Forgery (CSRF) e Injection, comuns também para aplicações web conforme OWASP TOP 10 (Passo 1).
O código tem o propósito de realizar consultas ordenadas com um fluxo de dados demonstrado pela classe Booking, a qual define o padrão de recebimento de dados por meio do método resolve. Esse método possui como parâmetro o order no que tange a ordenação “ascendente ou ou descendente” da consulta em banco de dados SQL realizada (Passo 2).
Refletindo sobre hipóteses (Passo 3), o pior cenário seria um agente externo ter acesso ou controle a todos os dados dessa aplicação e as chances disso acontecer depende da forma como é construída a consulta com o banco de dados. Portanto, o código pode contribuir para esse cenário na forma como ele constrói as consultas e no caso do nosso exercício, na forma como ele recebe o order do client que é a única “porta” que identificamos.
Agora, vamos subverter o propósito do código e testar uma única hipótese:
- Se alguém realizar alguma injeção no campo order com uma string “1 ASC — 1 DESC — DESC” terá como consequência a ordem dos resultados invertida.
Se olhar bem o código, verá que não há filtro algum ou validação adequada do source para o sink nesse fluxo de dados. O mais provável resultado será a reorganização dos resultados em ordem decrescente, confirmando nossa hipótese levantada de subversão do código.
Pronto, identificamos a vulnerabilidade do desafio, era um SQL Injection.
Habilidades que podem melhorar com a prática!
A prática leva à perfeição. Quanto mais desafios de codificação você fizer, mais habilidoso você será, além de construir novos métodos que vão agregar valor no seu Code Review.
Comece com os desafios mais fáceis e vá subindo. Em seguida, volte aos desafios que você completou e tente resolvê-los de forma diferente ou resolver desafios semelhantes. No entanto, encontrar problemas é apenas metade da batalha. Desenvolvedores também devem saber como corrigir vulnerabilidades e, mais importante, evitar sua recorrência.
Participar de desafios de codificação ajudará você a apontar os erros nas linhas de código facilmente. Além disso, capacitará suas habilidades técnicas para escrever códigos sem erros. Em nosso Twitter você pode encontrar desafios mensais e participar das discussões junto com especialistas de segurança.
No People and Culture da Conviso Platform, adotamos uma abordagem diferente para o treinamento de codificação seguro. Ensinamos desenvolvedores a identificar um problema de segurança, explorá-lo por conta própria e, em seguida, corrigi-lo modificando o código responsável por esse problema. Prático como resolver um desafio!
