Instalações de gems Ruby podem expor você a ataques de injeção em arquivos lock
17 de agosto de 2022
0 minutos de leituraNeste post, vamos analisar os pontos cegos de segurança da injeção em arquivos lock que uma gem Ruby pode expor por meio do arquivo Gemfile.lock. Antes disso, vamos apresentar brevemente o Ruby e o gerenciamento de dependências de terceiros no ecossistema RubyGems e Bundler.
Desenvolvedores web costumam trabalhar em projetos Ruby, mas geralmente se referem a eles como o popular framework open source para aplicações web Ruby on Rails. Ruby é a linguagem dinâmica e open source que está por trás do projeto.
Para tornar o código Ruby modular e reutilizável, ele é empacotado usando o projeto RubyGems, que costuma ser acessado pela linha de comando por meio do executável gem. Esses pacotes são chamados de gems e são referenciados em um arquivo de manifesto de pacotes chamado Gemfile.
O RubyGems agora vem incluído na própria linguagem Ruby e não precisa ser instalado separadamente. Embora um desenvolvedor possa instalar uma gem com um comando simples, como gem install <gem-name>, o gerenciamento geral de dependências transitivas, o travamento de versões e o controle de versões são bem mais complexos.
É aí que entra o projeto Bundler — o gerenciador de pacotes Ruby favorito da comunidade.
Manifesto Gemfile do Ruby e dependências transitivas
Projetos Ruby gerenciam suas dependências de terceiros definindo um arquivo Gemfile. Esse arquivo contém metadados do projeto e também lista todas as dependências usadas para compilá-lo.
Veja um exemplo de projeto Ruby com um Gemfile:
A definição source indica onde a ferramenta bundler pode encontrar essas bibliotecas de gems. O repositório https://rubygems.org é o registro padrão — e open source — do Rubygems.
Cada dependência é declarada nesse arquivo como uma definição de gem, com ou sem uma restrição de versão.
Como usar o bundler do Ruby para instalar e gerenciar gems
O Bundler — projeto open source de gerenciamento de gems — é responsável por instalar e resolver dependências. Para começar a usar o bundler e gerenciar suas dependências Ruby, primeiro você precisa ter o Ruby e o RubyGems instalados no ambiente. Depois, basta instalar a ferramenta bundler da seguinte forma:
Instalação de dependências Ruby e Gemfile.lock
Depois de instalar a ferramenta bundler, você pode instalar as dependências declaradas no manifesto Gemfile executando o seguinte comando:
Isso também equivale a executar bundle sem argumentos adicionais, por exemplo:
Para acompanhar as dependências instaladas, o bundler consulta o arquivo Gemfile.lock a fim de recriar o estado exato das versões de dependências em toda a árvore de dependências aninhadas. Se esse arquivo não existir, o bundler instalará as dependências e, em seguida, criará o arquivo Gemfile.lock.
Em alguns casos, talvez seja necessário especificar a opção --path para o comando bundle install, a fim de permitir a criação dos arquivos.
Após a execução, um log de instalação do bundler será semelhante a este:
Veja os dois principais resultados da instalação pelo bundler:
As dependências de gems de terceiros são instaladas no diretório
vendor/bundle.O arquivo
Gemfile.locké criado para registrar o estado exato das dependências em toda a árvore de dependências aninhadas, caso ele ainda não tenha sido criado.
Ao inspecionar o arquivo de lock das dependências, Gemfile.lock, encontramos as seguintes informações:
Injeção no arquivo lock Gemfile.lock do RubyGems
A injeção em arquivos lock é um vetor de ataque no qual um usuário mal-intencionado modifica o arquivo Gemfile.lock para incluir um conjunto de dependências diferente do manifesto Gemfile. Como arquivos lock como Gemfile.lock são gerados por máquinas, eles raramente são revisados por pessoas durante as revisões de código. Para piorar, quando há muitas alterações de código, a visualização de uma solicitação de pull com mudanças nesse arquivo mostra o arquivo lock recolhido.
Para demonstrar a gravidade e a frequência desse ataque, modificamos o trecho de código acima, de um arquivo Gemfile.lock, para incluir outro conjunto de dependências. Você consegue identificar quais pacotes maliciosos foram adicionados? Não é tão fácil quanto parece! Incluí a gem Ruby inventada rspec-assertions (~> 2.9.0) como dependência na árvore de rspec. Essa dependência nem sequer existe, mas poderia existir se eu criasse uma gem Ruby maliciosa para incluí-la entre as dependências que você instala.
O ataque é realizado modificando o arquivo Gemfile.lock e adicionando uma nova dependência aninhada à árvore de outra dependência. Por exemplo, considere o conteúdo a seguir de um arquivo lock (que mostra apenas o início do arquivo):
O arquivo Gemfile.lock acima representa uma resolução válida da árvore de dependências. No entanto, um usuário mal-intencionado pode atualizar essa árvore de dependências gerada pelo bundler para adicionar a gem legítima chamada digest, da seguinte forma:
Se você executar bundle no terminal para manter os pacotes atualizados, o bundler instalará a gem digest. Podemos inspecionar o diretório vendor/ para confirmar a presença da nova gem:
Nesse caso, a superfície de ataque permite que desenvolvedores desavisados — que podem receber contribuições para atualizar pacotes da comunidade ou de outras fontes não verificadas — insiram novas dependências de gems maliciosas no arquivo Gemfile.lock, envenenando o arquivo lock e colocando os responsáveis pela manutenção do projeto na mira.
A superfície de ataque da injeção em arquivos lock de gems Ruby
Qual é o impacto de incluir gems Ruby arbitrárias, controladas por um invasor, na árvore de dependências de uma aplicação Ruby?
Como a nova dependência de gem foi criada com intenções maliciosas, é improvável que o código-fonte a exija ou que outra dependência a invoque em tempo de execução. Mas poderíamos explorar um vetor de ataque semelhante que existe no gerenciador de pacotes npm do JavaScript, que permite executar comandos arbitrários durante a instalação das dependências.
Desde 2011, o ecossistema Ruby tomou uma decisão acertada: não permitir que os responsáveis pela manutenção de pacotes se conectem ao processo de instalação de dependências para executar comandos arbitrários. As referências a essa discussão estão em uma issue do GitHub, na qual explicam por que um script postinstall como o do npm é uma má ideia — e com razão.
Ainda assim, as gems Ruby permitem incluir código C nativo, que é compilado durante a instalação. É essa brecha que podemos explorar para viabilizar a execução de código arbitrário por meio da injeção em arquivos lock. Na verdade, esse método é usado na prática desde 2008 e ainda funciona para se conectar ao processo de instalação de gems Ruby.
Vamos criar uma gem Ruby para fins maliciosos
Vamos começar criando uma gem Ruby com instruções para contornar o mecanismo integrado da especificação Gemfile, que não permite nativamente a execução de código durante a instalação.
Dar nomes às coisas é difícil no desenvolvimento de software, mas não para nós. Vamos chamar nossa gem Ruby de woof. Este é o arquivo gemspec da nossa gem Ruby, chamado woof.gemspec:
Em seguida, crie o arquivo de origem principal da nossa gem Ruby, lib/woof.rb:
Depois, podemos criar o arquivo de extensão nativa do Ruby extconf.rb, que permite declarar as informações necessárias para criar makefiles, fornecer instruções para estender o Ruby com extensões C nativas e, em geral, executar qualquer código Ruby.
Crie o arquivo ext/woof/extconf.rb com o seguinte conteúdo:
O trecho de código da extensão acima é um exemplo simples de como criar uma extensão C nativa para Ruby. O método create_makefile é um método Ruby usado para criar um makefile para uma extensão C nativa. O método create_makefile recebe um único argumento: o nome da extensão. Esse nome deve ser igual ao nome da gem Ruby.
A primeira linha do código da extensão chama o método File.write, um método Ruby que grava um arquivo no sistema de arquivos. O primeiro argumento de File.write é o caminho do arquivo que será gravado, e o segundo é o conteúdo a ser gravado.
Ao simularmos um pacote malicioso (inofensivo), vemos que, ao instalar nossa gem Ruby woof, um novo arquivo será criado no sistema de arquivos. O arquivo se chamará /tmp/woof e estará vazio.
Para que a gem Ruby seja instalada corretamente, precisamos fornecer um arquivo hello.c válido. Pode ser um programa simples de *hello world*, como este dos documentos padrão do Ruby:
Por fim, podemos compilar esta gem Ruby. No diretório raiz do projeto, execute o seguinte comando:
Se tudo der certo, uma nova gem será criada no diretório raiz do projeto com o nome woof-1.0.0.gem.
Para testar se esta gem Ruby funciona como esperado, podemos instalá-la e verificar se um novo arquivo /tmp/woof foi criado (você pode confirmar que ele não existe antes da instalação do pacote). Siga estas etapas para instalar a gem Ruby em uma pasta local:
Crie um novo diretório temporário, por exemplo,
mkdir ~/ruby-gem-test.Nesse novo diretório, crie um diretório chamado
vendor.Instrua o Ruby a usar o novo diretório
vendorcomo diretório de gems e instale nele a gemwoof, desta forma:gem install --local /path/to/woof-1.0.0.gem --install-dir 'vendor'
Para confirmar que o ataque de execução arbitrária de código foi aplicado com sucesso como método de instalação de uma gem Ruby, verifique se um novo arquivo /tmp/woof foi criado.
Como evitar a injeção em arquivos lock de gems Ruby
Como mostramos ao longo deste artigo, o processo de instalação de gems Ruby permite que os responsáveis pela manutenção de pacotes executem código arbitrário durante a instalação de dependências. Essa é uma poderosa superfície de ataque, que pode ser explorada junto com ataques de injeção em arquivos lock de uma aplicação Ruby e, de forma mais ampla, no contexto da segurança da cadeia de suprimentos do ecossistema Ruby.
Infelizmente, ao contrário do gerenciador de pacotes npm, que inclui a opção de linha de comando --ignore-script, a própria CLI gem do Ruby não permite desativar a compilação de extensões C nativas durante a instalação. Assim, os usuários ficam vulneráveis à capacidade de responsáveis pela manutenção de pacotes de terceiros executarem código arbitrário.
Para ajudar a reduzir esse tipo de risco, recomendamos:
Gerencie as dependências de gems Ruby e suas atualizações usando um bot automatizado para atualizar as versões conforme necessário e fornecer correções de segurança sempre que possível. Faça o mesmo com as alterações no arquivo lock
Gemfile.lock.Se você aceitar contribuições de desenvolvedores para os arquivos
GemfileeGemfile.lock, revise cuidadosamente essas alterações de código e examine cada linha modificada.Prefira instalar dependências com
bundle install --deployment. Esse comando calcula a árvore completa de dependências de todas as gems Ruby e interrompe a instalação quando o arquivoGemfile.locknão corresponde exatamente às gems que deveriam ser instaladas peloGemfile.
Esta pesquisa se baseia em um trabalho original anterior que realizei (Liran Tal), em 2019, para divulgar os riscos da injeção em arquivos lock no ecossistema JavaScript.
Mais recursos sobre segurança em Ruby:
Comece a resolver desafios de capture the flag
Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.



