Skip to main content

Guia definitivo para gerenciar dependências de gems Ruby

Escrito por
blog feature ruby security

5 de agosto de 2022

0 minutos de leitura

Assim como outras linguagens de programação, Ruby tem todo um ecossistema de bibliotecas open source de terceiros, chamadas de gems ou, às vezes, Ruby gems. Essas gems são criadas pela comunidade e estão disponíveis no RubyGems.org, o registro oficial de bibliotecas Ruby. Como acontece em outros ecossistemas open source, agentes mal-intencionados podem publicar código malicioso de propósito, com backdoors ou recursos para roubar credenciais. Por isso, é fundamental ter atenção ao gerenciar e auditar suas gems open source para Ruby.

Neste artigo, vou apresentar os conceitos e as ferramentas que compõem o ecossistema de dependências do Ruby e responder a algumas das dúvidas mais comuns de quem desenvolve em Ruby.

A linguagem de programação Ruby

Talvez você conheça o Ruby on Rails (RoR), o popular framework de desenvolvimento web que contribuiu muito para o sucesso e a popularidade do Ruby. O RoR é baseado no Ruby, uma linguagem dinâmica, multiplataforma e interpretada. Seus arquivos de código-fonte são facilmente reconhecidos pela extensão .rb.

O que são Ruby gems?

Gems são bibliotecas de código-fonte empacotadas, modulares, independentes e fáceis de reutilizar em diferentes projetos.

O que é RubyGems e qual é a diferença em relação ao Bundler?

RubyGems é o registro central de pacotes onde gems Ruby open source de terceiros são compartilhadas. Também é o gerenciador oficial de pacotes Ruby, conhecido como gem quando usado pela interface de linha de comando.

Página inicial do RubyGems com campo de busca, título “Encontre, instale e publique RubyGems”, número de downloads e botão “Instalar RubyGems”.

RubyGems é uma ótima maneira de dar aos desenvolvedores acesso a um ecossistema com centenas de milhares de bibliotecas open source para usar em seus projetos.

A CLI gems do próprio RubyGem já vem instalada em um ambiente Ruby funcional. Assim, você pode começar a instalar suas dependências open source favoritas, como:

sh
gem install rails

A CLI gems, no entanto, gerencia cada dependência individualmente: busca os pacotes em um registro e os instala no sistema de arquivos para que estejam disponíveis e possam ser importados facilmente nos projetos Ruby.

O projeto Bundler, que tecnicamente também é uma Ruby gem e é instalado como tal, facilita a instalação de gems sem precisar instalar uma por uma. Ele também permite criar uma árvore de dependências determinística e consistente usando um arquivo de manifesto chamado Gemfile e seu arquivo de bloqueio correspondente, Gemfile.lock. Este último fixa as dependências de toda a árvore e garante a consistência entre instalações ao longo do tempo.

Como o Bundler é uma gem, começar a usá-lo é simples:

sh
gem install bundler

Esse comando disponibiliza a ferramenta de linha de comando bundler para os desenvolvedores.

O Gemfile do Ruby

O arquivo chamado Gemfile é um manifesto de texto que descreve todas as dependências que devem ser usadas pelo projeto, além de outras instruções de metadados disponibilizadas pela ferramenta bundler.

Um Gemfile simples seria assim:

ruby
source 'https://rubygems.org'

gem ‘rails’, ‘4.2.3’
gem ‘sqlite3’

O Gemfile acima define a entrada source, que indica o repositório remoto do qual o Bundler baixará todas as gems. Cada linha gem define uma biblioteca open source, a versão a ser baixada e a origem onde ela pode ser encontrada.

Veja a seguir um exemplo mais complexo, que mostra como as Ruby gems podem vir de diferentes locais:

ruby

gem 'nokogiri'
gem 'nokogiri', git: 'https://github.com/sparklemotion/nokogiri.git', branch: main'
gem 'nokogiri', github: ‘sparklemotion/nokogiri’
gem 'nokogiri', path: '/path/to/local/nokogiri’

Mais detalhes sobre essas origens estão descritos em profundidade na documentação oficial do Bundler no Git.

O que compõe uma Ruby gem?

Uma Ruby gem é um código-fonte Ruby empacotado e modular que pode ser reutilizado por outras Ruby gems, por aplicações Ruby ou, de modo geral, por usuários que interagem com ele pela linha de comando, como vimos anteriormente com o Bundler.

Em geral, as Ruby gems seguem uma convenção que inclui o código-fonte Ruby, testes, possíveis arquivos executáveis disponibilizados no caminho de arquivos do usuário e, acima de tudo, um arquivo de especificação da Ruby gem, conhecido como Gemspec.

sh 
base64ness/
├─ lib/
│  ├─ base64.rb
├─ bin/
│  ├─ base64ness
│  ├─ index.html
│  ├─ robots.txt
├─ test/
│  ├─ base64
├─ base64ness.gemspec
├─ Rakefile
├─ Gemfile
├─ Gemfile.lock
├─ README.md

Diretórios como test/ e bin/ são relativamente fáceis de entender: eles contêm o código de teste dessa dependência e o arquivo executável que será empacotado e instalado com ela. Outros arquivos importantes são Gemfile e Gemfile.lock, que vimos quando apresentamos o Bundler para gerenciar Ruby gems e seu arquivo de bloqueio, garantindo que as instalações sejam determinísticas e possam ser repetidas com segurança.

O Rakefile é um arquivo associado ao projeto open source Rake, um utilitário de build para projetos Ruby semelhante ao make. Por isso, o nome é parecido com Makefile. Veja um exemplo do conteúdo:

ruby
require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "test"
  t.verbose = true
  t.test_files = FileList["test/**/*.rb"]
end

desc "Run tests"
task default: :test

O Rakefile importa as classes necessárias da gem Rake e define uma tarefa test. Quando o comando rake test é executado nesse diretório, ele instrui o programa Rake a localizar, usando um padrão glob, todos os arquivos de teste na pasta test/ e executá-los. Para saber mais sobre o formato do Rakefile, consulte a documentação do repositório do projeto Rake.

Outro arquivo novo que você talvez tenha notado na árvore de arquivos acima é o gemspec, geralmente chamado pelo mesmo nome do pacote, como base64ness.gemspec. O gemspec descreve a Ruby gem e fornece metadados sobre essa dependência. Ele ajuda o registro RubyGems a rastrear e analisar essas informações e disponibilizá-las para quem procura Ruby gems.

Veja um exemplo do arquivo base64ness.gemspec da Ruby gem base64 mencionada acima:

ruby
Gem::Specification.new do |s|
  s.name        = "base64ness"
  s.version     = "1.0.0"
  s.summary     = "It does magic with base64!"
  s.description = "This Ruby gem adds extended utilities for base64 encoding and decoding tasks such as making them URL compliant."
  s.authors     = ["Liran Tal"]
  s.email       = "liran@snyk.io"
  s.files       = ["lib/base64.rb"]
  s.homepage    =
    "https://rubygems.org/gems/base64ness"
  s.license       = "MIT"
end

O site RubyGems oferece uma documentação completa da classe Specification mencionada acima. Ela inclui outros metadados associados a uma Ruby gem, recursos adicionais que permitem aos mantenedores de Ruby definir executáveis, adicionar extensões C às Ruby gems — que, por meio de compilação cruzada, oferecem suporte a módulos nativos — e muito mais.

Dica: se você quiser saber o que há dentro de uma Ruby gem específica, pode descompactá-la em um diretório e examinar o código-fonte assim:

sh
gem unpack your-ruby-gem.gem --target /path/to/gem-directory

Como controlar a instalação de dependências de Ruby gems

Você pode agrupar logicamente as dependências em categorias específicas, de acordo com sua finalidade. Por exemplo, é possível ter dependências de Ruby gems de produção e gems de desenvolvimento, que você instala apenas ao desenvolver localmente.

Para separá-las, você pode definir o seguinte manifesto Gemfile:

ruby
group :production do
  gem ‘rails’,
end

group :development do
  gem ‘rspec-rails’
end

Depois, você pode instalar as dependências por categoria usando o Bundler. Por exemplo, o comando abaixo instala apenas as dependências de produção:

sh
bundle install --without development

Por que precisamos de um Gemfile.lock?

Se você pudesse definir suas dependências apenas usando o manifesto de pacotes Gemfile do Bundler, estaria sujeito às seguintes limitações:

  1. Apenas as dependências diretas de Ruby gems são documentadas.

  2. Essas dependências diretas de Ruby gems usam uma versão pouco específica e definida de forma imprecisa para o pacote. Ela pode buscar a versão mais recente ou apenas a mais recente dentro de um intervalo de versões semânticas.

  3. Mesmo que você fixe as dependências diretas de Ruby gems em versões específicas, a cada nova instalação elas ainda podem resolver para uma versão inesperada de uma gem dependente.

Isso significa que, sem um arquivo de bloqueio para fixar todas as dependências aninhadas de Ruby gems instaladas no projeto, as versões das gems instaladas podem variar de forma imprevisível. Provavelmente, essa é a última coisa que você quer que aconteça em um ambiente automatizado de CI ou build, ou com outros desenvolvedores Ruby que colaboram no projeto com você.

É aí que entra o Gemfile.lock:

ruby
GEM
  remote: https://rubygems.org/
  specs:
    actioncable (7.0.3.1)
      actionpack (= 7.0.3.1)
      activesupport (= 7.0.3.1)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
    actionmailbox (7.0.3.1)
      actionpack (= 7.0.3.1)
      activejob (= 7.0.3.1)
      activerecord (= 7.0.3.1)
      activestorage (= 7.0.3.1)
      activesupport (= 7.0.3.1)
      mail (>= 2.7.1)
      net-imap
      net-pop
      net-smtp

PLATFORMS
  aarch64-linux

DEPENDENCIES
  bootsnap
  capybara
  debug
  importmap-rails
  jbuilder
  puma (~> 5.0)
  rails (~> 7.0.3, >= 7.0.3.1)
  rails_admin!
  selenium-webdriver
  sprockets-rails
  sqlite3 (~> 1.4)
  stimulus-rails
  turbo-rails
  tzinfo-data
  web-console
  webdrivers

RUBY VERSION
   ruby 3.1.2p20

BUNDLED WITH
   2.3.18

Vamos analisar a especificação e o formato desse arquivo de bloqueio de dependências de gems:

  • A diretiva GEM inicia o bloco que lista todas as dependências de Ruby gems em uma árvore aninhada, mostrando as dependências diretas e transitivas e suas versões. Elas são identificadas na diretiva specs. A diretiva remote informa à ferramenta bundler de onde buscar essas Ruby gems para instalá-las.

  • A diretiva PLATFORMS é uma lista aberta de plataformas de destino para as quais é necessário compilar Ruby gems nativas, devido à cadeia de compilação cruzada exigida, por exemplo, para extensões Ruby escritas em C.

  • A diretiva RUBY VERSION é opcional e especifica a versão do runtime Ruby usada para criar este arquivo de bloqueio Gemfile.lock.

  • A diretiva BUNDLED WITH especifica a versão do Bundler usada para criar o arquivo de bloqueio. Desenvolvedores que usam versões mais antigas devem considerar atualizá-las para se manter em dia e garantir resultados consistentes e previsíveis ao instalar Ruby gems.

bundler-audit e a importância de reforçar a segurança das Ruby gems

Um ótimo ecossistema também traz grandes responsabilidades. É aí que entra o projeto comunitário bundler-audit. Ele é uma Ruby gem que verifica as dependências especificadas no arquivo de bloqueio do projeto (Gemfile.lock) e as compara com um banco de dados de Ruby gems vulneráveis. Em seguida, informa se encontrou versões associadas a uma ou mais vulnerabilidades.

Como começar a usar bundler-audit para verificar vulnerabilidades em Ruby gems

Para começar a procurar vulnerabilidades com o bundler-audit, primeiro instalamos a ferramenta, como qualquer outra Ruby gem:

sh
gem install bundler-audit

Depois de instalado, o bundler-audit precisa baixar avisos de segurança de fontes como o ruby-adivsory-db, mantido pela comunidade, que por sua vez obtém dados do GitHub Advisory e do banco de dados de vulnerabilidades open source mantido pelo Google:

sh
bundler-audit update

Se tudo der certo, o comando bundler-audit exibirá algo parecido com o exemplo a seguir, indicando que os dados estão atualizados:

sh
Updating ruby-advisory-db ...
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
From https://github.com/rubysec/ruby-advisory-db
 * branch            master     -> FETCH_HEAD
Already up to date.
Updated ruby-advisory-db
ruby-advisory-db:
  advisories:596 advisories
  last updated:2022-07-18 13:57:38 -0700
  commit:66f047bdcb4bbda76857f9ba668b7d71b641b28b

Em seguida, podemos executar bundle-audit sem argumentos adicionais para verificar se há vulnerabilidades de segurança. No meu projeto local de Ruby on Rails, o resultado foi:

No vulnerabilities found

Parece bom, mas pode ser enganoso e expor você a riscos desnecessários. Na verdade, trata-se de um falso negativo, pois existem vulnerabilidades de segurança em algumas das versões de dependências que instalei.

Com todo o respeito ao trabalho da comunidade para criar ferramentas de segurança, provavelmente faltam pessoas e recursos para garantir uma cobertura adequada de vulnerabilidades. É aí que entra o scanner de segurança do Snyk Open Source.

O Snyk oferece uma ferramenta de CLI gratuita, baseada em uma empresa de segurança que prioriza os desenvolvedores e conta com um amplo banco de dados de relatórios de vulnerabilidades em Ruby gems. Você pode instalar a ferramenta e verificar suas dependências.

Como proteger Ruby gems gratuitamente com o Snyk

Vamos testar o Snyk e conferir quais vulnerabilidades de segurança ele encontra no nosso projeto Rails. Se você usa um MacBook Pro M1 como eu, execute:

sh
curl https://static.snyk.io/cli/latest/snyk-linux-arm64 -o snyk
chmod +x snyk
mv snyk /usr/local/bin

Isso fará o download do binário independente do Snyk, dará permissão para executá-lo e o disponibilizará no seu PATH. Se você precisar de outro método de instalação, como brew no macOS, scoop no Windows ou outro, consulte a documentação para começar a usar a CLI do Snyk.

Depois de instalar a CLI do Snyk, podemos executá-la assim:

sh
snyk test

Se esta for a primeira vez que você a executa, verá a seguinte mensagem:

sh
`snyk` requires an authenticated account. Please run `snyk auth` and try again.

Nesse momento, digite snyk auth e siga as instruções para copiar uma URL de autenticação no navegador. Faça login ou crie uma conta para receber sua chave de API. Em pouco tempo, você estará de volta à linha de comando para continuar a verificação:

sh
snyk test

Testing /home/app/myapp...

Tested 80 dependencies for known issues, found 2 issues, 59 vulnerable paths.

Issues with no direct upgrade or patch:
  ✗ Information Exposure [Medium Severity][https://security.snyk.io/vuln/SNYK-RUBY-ACTIONCABLE-20338] in actioncable@7.0.3.1
    introduced by rails@7.0.3.1 > actioncable@7.0.3.1 and 1 other path(s)
  No upgrade or patch available
  ✗ Web Cache Poisoning [Medium Severity][https://security.snyk.io/vuln/SNYK-RUBY-RACK-1061917] in rack@2.2.4
    introduced by capybara@3.37.1 > rack@2.2.4 and 56 other path(s)
  No upgrade or patch available

Organization:      snyk-demo-567
Package manager:   rubygems
Target file:       Gemfile
Project name:      myapp
Open source:       no
Project path:      /home/app/myapp
Licenses:          enabled

Bem, são 2 problemas e 59 caminhos vulneráveis… nosso projeto não é tão seguro quanto pensávamos. No momento, parece não haver versões mais recentes para atualizar essas bibliotecas na árvore de dependências aninhadas. Para garantir que o Snyk monitore continuamente o Gemfile do nosso projeto e procure correções em novas versões, podemos fazer uma das seguintes coisas:

  1. Acesse https://apps.snyk.io e conecte o Snyk ao repositório de código-fonte no GitHub ou em outro lugar. Assim, o Snyk poderá monitorar esse projeto em segundo plano.

  2. Execute o comando snyk monitor, que criará um snapshot dos arquivos atuais Gemfile e Gemfile.lock e os monitorará continuamente. Lembre-se de executar esse comando em um processo de CI ou build automatizado para que, sempre que esses arquivos de manifesto de pacotes Ruby forem atualizados, um novo snapshot seja enviado ao Snyk para monitoramento e acompanhamento.

Devo usar bundle-audit ou Snyk?

Use os dois. Não digo isso por diplomacia: eles realmente se complementam das seguintes maneiras:

  1. O Snyk oferece um banco de dados abrangente de vulnerabilidades de segurança no ecossistema Ruby, selecionado manualmente e mantido atualizado, muitas vezes até antes do bundle-audit e de outras ferramentas.

  2. O Bundle-audit faz outras verificações além de analisar vulnerabilidades, como confirmar que seus gems Ruby não estão sendo obtidos de fontes inseguras, como http:// e git://, que poderiam permitir um ataque man-in-the-middle ou de injeção no lockfile para adulterar o código-fonte recebido e executado.

Resumo

Aprendemos a gerenciar gems Ruby como dependências do projeto, por que é importante usar um lockfile e apresentamos outros aspectos e boas práticas recomendadas, como usar dependências Ruby com segurança, analisando-as com ferramentas como Snyk e bundler-audit.

Outros recursos sobre Ruby:

Se você também desenvolve em JavaScript, recomendo muito a leitura de o que é package-lock.json e como um lockfile funciona para pacotes Yarn e npm.

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.

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está avançando mais rápido do que os fluxos de trabalho de segurança tradicionais. O AppSec agentivo da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

blog feature ai violet
Blog

VulnBump: agentes de IA conseguem corrigir dependências vulneráveis sem quebrar o build?

O VulnBump testa agentes de IA em 307 atualizações reais de dependências. Uma correção só é considerada válida se eliminar a vulnerabilidade e mantiver o build e os testes funcionando.

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.