Skip to main content

Guía definitiva para administrar las dependencias de las gemas de Ruby

Escrito por
blog feature ruby security

5 de agosto de 2022

0 minutos de lectura

Ruby, al igual que otros lenguajes de programación, cuenta con todo un ecosistema de bibliotecas de código abierto de terceros, conocidas como gems o, a veces, Ruby gems. La comunidad crea estas gems, que están disponibles en RubyGems.org, el registro oficial de bibliotecas de Ruby. Como ocurre en otros ecosistemas de código abierto, los actores de amenazas pueden publicar código malicioso de forma intencional, por ejemplo, con puertas traseras o que recopila credenciales. Por eso, es fundamental prestar atención a cómo administras y auditas tus gems de código abierto de Ruby.

En este artículo, repasaré los conceptos y las herramientas que conforman el ecosistema de dependencias de Ruby y responderé algunas de las preguntas frecuentes de los desarrolladores de Ruby.

El lenguaje de programación Ruby

Quizás hayas oído hablar de Ruby on Rails (RoR), el popular framework de desarrollo web que contribuyó mucho al éxito y la popularidad de Ruby. RoR se basa en Ruby, un lenguaje dinámico, multiplataforma e interpretado. Sus archivos de código fuente se reconocen fácilmente por la extensión .rb.

¿Qué son las gems de Ruby?

Las gems son bibliotecas de código fuente empaquetadas, modulares e independientes, que se pueden reutilizar fácilmente en distintos proyectos.

¿Qué es RubyGems y en qué se diferencia de Bundler?

RubyGems es el registro central de paquetes donde se comparten gems de Ruby de código abierto de terceros y también es el administrador oficial de paquetes de Ruby, conocido como gem cuando se usa desde la interfaz de línea de comandos.

Página de inicio de RubyGems con un campo de búsqueda, el encabezado «Busca, instala y publica RubyGems», el número de descargas y un botón para instalar RubyGems.

RubyGems es una excelente manera de permitir que los desarrolladores aprovechen un ecosistema de cientos de miles de bibliotecas de código abierto y las usen en sus proyectos.

La CLI gems de RubyGems viene preinstalada con un entorno de Ruby funcional, para que los desarrolladores puedan empezar de inmediato a instalar sus dependencias de código abierto favoritas. Por ejemplo:

sh
gem install rails

Sin embargo, la CLI gems administra cada dependencia por separado: las obtiene de un registro y las instala en el sistema de archivos para que estén disponibles y se puedan importar fácilmente a los proyectos de código Ruby.

El proyecto Bundler, que técnicamente es una gem de Ruby y se instala como tal, facilita la instalación de gems una por una y permite tener un árbol de dependencias determinista y coherente mediante un archivo de manifiesto llamado Gemfile y su archivo de bloqueo correspondiente, Gemfile.lock. Este último fija las dependencias de todo el árbol y garantiza que las instalaciones sigan siendo coherentes con el tiempo.

Como Bundler es una gem, empezar a usarlo es tan fácil como:

sh
gem install bundler

Este comando pone la herramienta de línea de comandos bundler a disposición de los desarrolladores.

El Gemfile de Ruby

El archivo llamado Gemfile es un archivo de texto de manifiesto que describe todas las dependencias que debe usar el proyecto, así como otras instrucciones de metadatos que expone la herramienta bundler.

Un Gemfile sencillo sería así:

ruby
source 'https://rubygems.org'

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

El Gemfile anterior define la entrada source, que es el repositorio remoto desde el que Bundler descargará todas las gems. Cada línea gem define una biblioteca de código abierto, la versión que se debe obtener y la fuente donde encontrarla.

A continuación, se muestra un ejemplo más complejo de cómo se obtienen las Ruby Gems desde distintas ubicaciones:

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’

La documentación oficial de Bundler en Git describe estas fuentes con más detalle.

¿Qué contiene una gem de Ruby?

Una gem de Ruby es código fuente de Ruby empaquetado y modular que pueden reutilizar otras gems de Ruby, aplicaciones de Ruby o, en general, los usuarios que interactúan con ella desde la línea de comandos, como vimos antes con Bundler.

Las gems de Ruby suelen seguir una convención que incluye el código fuente de Ruby, pruebas, posibles archivos ejecutables disponibles en la ruta de archivos del usuario y, lo más importante, el archivo de especificación de la gem de Ruby, conocido como Gemspec.

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

Algunos directorios, como test/ y bin/, deberían ser bastante fáciles de entender, ya que contienen el código de prueba de esta dependencia y el archivo ejecutable que se incluirá e instalará con ella. Otros archivos que cabe mencionar son Gemfile y Gemfile.lock, que vimos al presentar Bundler para administrar las gems de Ruby y su archivo de bloqueo, con el fin de garantizar instalaciones deterministas y seguras que se puedan repetir.

El Rakefile es un archivo asociado con el proyecto de código abierto Rake, una herramienta de compilación para proyectos de Ruby similar a make. De ahí que su nombre sea parecido a Makefile. Este es un ejemplo de su contenido:

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

El Rakefile importa las clases necesarias de la gem Rake y define una tarea test. Cuando se ejecuta el comando rake test en este directorio, se le indica al programa Rake que busque, mediante un patrón glob, todos los archivos de prueba en la carpeta test/ y luego los ejecute. Para obtener más información sobre el formato del Rakefile, consulta la documentación del repositorio del proyecto Rake.

Otro archivo nuevo que quizás hayas notado en el árbol de archivos anterior es el archivo gemspec, que suele llevar el mismo nombre que el paquete, por ejemplo, base64ness.gemspec. El gemspec describe la gem de Ruby y proporciona metadatos sobre esta dependencia. También ayuda al registro RubyGems a hacer un seguimiento de esta información, analizarla y ponerla a disposición de quienes buscan gems de Ruby.

Este es un ejemplo del archivo base64ness.gemspec de la gem de Ruby base64 mencionada anteriormente:

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

El sitio web de RubyGems ofrece documentación completa sobre la clase Specification mencionada antes, que incluye metadatos adicionales para una gem de Ruby, funciones ampliadas que permiten a los responsables de Ruby definir ejecutables y agrupar extensiones de C en gems de Ruby, las cuales, mediante compilación cruzada, ofrecen capacidades de módulos nativos, entre otras cosas.

Consejo: si te interesa saber qué contiene una gem de Ruby en particular, puedes descomprimirla en un directorio y examinar el código fuente así:

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

Controlar la instalación de dependencias de gems de Ruby

Las dependencias se pueden agrupar de forma lógica en categorías específicas según su función. Por ejemplo, puedes tener dependencias de gems de Ruby de producción y gems de Ruby de desarrollo, que solo instalarías mientras desarrollas de forma local.

Para diferenciarlas, puedes definir el siguiente manifiesto Gemfile:

ruby
group :production do
  gem ‘rails’,
end

group :development do
  gem ‘rspec-rails’
end

Luego puedes instalar dependencias por categoría con Bundler. Por ejemplo, el siguiente comando solo instalaría las dependencias de producción:

sh
bundle install --without development

¿Por qué necesitamos un Gemfile.lock?

Si solo pudieras definir tus dependencias mediante el manifiesto de paquetes Gemfile de bundler, tendrías que enfrentar las siguientes limitaciones:

  1. Solo se documentan las dependencias directas de gems de Ruby.

  2. Estas dependencias directas de gems de Ruby usan una versión del paquete poco específica y definida de forma imprecisa. Podrían obtener la versión más reciente o solo la más reciente dentro de un rango de versiones semánticas.

  3. Aunque fijes las versiones de estas dependencias directas de gems de Ruby, cada nueva instalación podría resolver una versión inesperada de una gem dependiente.

Esto significa que, sin un archivo de bloqueo que fije todas las dependencias anidadas de las gems de Ruby instaladas para tu proyecto, se instalarían versiones indeterminadas de las gems. Probablemente sea lo último que quieras que ocurra en un entorno automatizado de CI o compilación, o con otros desarrolladores de Ruby que colaboran contigo en el proyecto.

Por eso existe 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

Revisemos la especificación y el formato de este archivo de bloqueo de dependencias de gems:

  • La directiva GEM inicia el bloque que enumera todas las dependencias de gems de Ruby en un árbol anidado, con las dependencias directas y transitivas y sus versiones. Estas aparecen bajo la directiva specs. La directiva remote indica a la herramienta bundler desde dónde obtener estas gems de Ruby para instalarlas.

  • La directiva PLATFORMS contiene una lista abierta de plataformas de destino para las que se requiere compilar gems nativas de Ruby, debido, por ejemplo, a la cadena de compilación cruzada necesaria para las extensiones de Ruby escritas en C.

  • La directiva RUBY VERSION es opcional y especifica la versión del entorno de ejecución de Ruby que se usó al crear este archivo de bloqueo Gemfile.lock.

  • La directiva BUNDLED WITH especifica la versión de Bundler que se usó para crear el archivo de bloqueo. Los desarrolladores que usan versiones anteriores deberían considerar actualizarla para estar al día y mantener resultados de instalación de gems de Ruby coherentes y consistentes.

bundler-audit y por qué necesitamos mejorar la seguridad de las gems de Ruby

Un gran ecosistema conlleva una gran responsabilidad. Así es. Aquí es donde entra en juego el proyecto comunitario bundler-audit. Es una gem de Ruby que analiza las dependencias especificadas en el archivo de bloqueo del proyecto (Gemfile.lock), las compara con una base de datos de gems de Ruby vulnerables e informa si encuentra versiones afectadas por una o más vulnerabilidades.

Cómo empezar a analizar vulnerabilidades de gems de Ruby con bundler-audit

Para empezar a analizar vulnerabilidades con bundler-audit, primero lo instalamos, como cualquier otra gem de Ruby:

sh
gem install bundler-audit

Una vez instalado, bundler-audit debe descargar avisos de seguridad de fuentes como ruby-adivsory-db, una base de datos mantenida por la comunidad que obtiene datos de GitHub Advisory y de la base de datos Open Source Vulnerability, mantenida por Google:

sh
bundler-audit update

Si todo sale bien, el comando bundler-audit debería mostrar algo parecido a lo siguiente para indicar que está todo actualizado:

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

Después, podemos ejecutar bundle-audit sin argumentos adicionales para buscar vulnerabilidades de seguridad. En mi proyecto local de Ruby on Rails, mostró lo siguiente:

No vulnerabilities found

Parece que todo está bien, pero esto puede ser engañoso y exponerte a riesgos innecesarios. De hecho, se trata de un falso negativo, porque sí existen vulnerabilidades de seguridad en algunas de las versiones de dependencias que tengo instaladas.

Con todo el respeto por el trabajo de la comunidad para crear herramientas de seguridad, es probable que no cuente con suficiente personal ni recursos para ofrecer una cobertura adecuada de vulnerabilidades. Aquí es donde entra en juego el escáner de seguridad de Snyk Open Source.

Snyk ofrece una herramienta CLI gratuita, respaldada por una empresa de seguridad que prioriza a los desarrolladores y cuenta con una amplia base de datos de informes de vulnerabilidades en gems de Ruby. Puedes instalarla para analizar tus dependencias.

Cómo proteger las gems de Ruby gratis con Snyk

Probemos Snyk y veamos qué vulnerabilidades de seguridad encuentra en nuestro proyecto de Rails. Si tienes una MacBook Pro M1 como yo, ejecuta lo siguiente:

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

Esto descargará el binario independiente de Snyk, lo hará ejecutable y lo agregará a tu ruta. Si necesitas otro método de instalación, como brew para macOS o scoop para Windows, consulta la documentación de inicio con la CLI de Snyk.

Una vez instalada la CLI de Snyk, podemos ejecutarla así:

sh
snyk test

Si es la primera vez que la usas, verás el siguiente mensaje:

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

A continuación, debes escribir snyk auth y seguir las instrucciones para copiar una URL de autenticación en el navegador. Inicia sesión o regístrate para recibir tu clave de API. Enseguida volverás a la línea de comandos y podrás continuar con el análisis:

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

Vaya, 2 problemas y 59 rutas vulnerables… no estamos tan seguros como pensábamos. Por ahora, parece que no hay versiones más recientes a las que actualizar estas bibliotecas en el árbol de dependencias anidadas. Para asegurarnos de que Snyk supervise continuamente el Gemfile de nuestro proyecto y busque nuevas correcciones de versiones que podamos aplicar, podemos hacer lo siguiente:

  1. Ve a https://apps.snyk.io y conecta Snyk al repositorio de código fuente en GitHub o en otro lugar. Esto permitirá que Snyk supervise este proyecto en segundo plano.

  2. Ejecuta el comando snyk monitor, que tomará una instantánea de los archivos Gemfile y Gemfile.lock actuales y los supervisará continuamente. Ten en cuenta que debes ejecutarlo desde una compilación automatizada o de CI para que, cada vez que se actualicen esos archivos de manifiesto de paquetes de Ruby, se envíe una nueva instantánea a Snyk para que la supervise y le dé seguimiento.

¿Debo usar bundle-audit o Snyk?

Usa ambos. No se trata de una respuesta diplomática, sino de que realmente se complementan de las siguientes maneras:

  1. Snyk ofrece una amplia base de datos de vulnerabilidades de seguridad en el ecosistema de Ruby, seleccionadas y actualizadas manualmente, a menudo incluso antes que bundle-audit y otras herramientas.

  2. Bundle-audit realiza otras comprobaciones además de analizar vulnerabilidades, como verificar que tus gemas de Ruby no se descarguen de fuentes inseguras, como http:// y git://, que permitirían un ataque de intermediario o de inyección en el archivo de bloqueo para alterar el código fuente que recibes y ejecutas.

Resumen

Aprendimos a administrar las gemas de Ruby como dependencias de proyectos, por qué es importante usar un archivo de bloqueo y describimos otros aspectos y prácticas recomendadas, como usar dependencias de Ruby de forma segura mediante su análisis con herramientas como Snyk y bundler-audit.

Otros recursos sobre Ruby:

Si también desarrollas en JavaScript, te recomiendo leer qué es package-lock.json y cómo funciona un archivo de bloqueo para los paquetes de Yarn y npm.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Leer más

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

Blog

Por qué los agentes de programación con IA siguen generando fallas de control de acceso

Los agentes de programación con IA pueden generar lógica de autorización que compila y supera la revisión, pero expone los datos de un inquilino a otro. Descubre por qué es difícil detectar el control de acceso roto y cómo prevenirlo.