Las instalaciones de gemas de Ruby pueden exponerte a ataques de inyección en el lockfile
17 de agosto de 2022
0 minutos de lecturaEn esta publicación, analizaremos los puntos ciegos de seguridad de la inyección en el lockfile que una gema de Ruby podría exponer a través de su Gemfile.lock. Como preámbulo, comenzaremos con una breve introducción a Ruby y a la gestión de dependencias de terceros con RubyGems y Bundler.
Los desarrolladores web suelen trabajar en proyectos de Ruby, pero generalmente se refieren a ellos como el popular framework de código abierto para aplicaciones web Ruby on Rails. Ruby es el lenguaje dinámico de código abierto subyacente que impulsa el proyecto.
Para que el código de Ruby sea modular y reutilizable, se empaqueta mediante el proyecto RubyGems, con el que suele interactuarse desde la línea de comandos a través del ejecutable gem. Estos paquetes se conocen como gems y se especifican en un archivo de manifiesto de paquetes llamado Gemfile.
RubyGems ahora viene incluido con el propio lenguaje Ruby y no es necesario instalarlo por separado. Aunque un desarrollador podría instalar una gema con un comando tan sencillo como gem install <gem-name>, la gestión general de dependencias transitivas, la fijación de versiones y el control de versiones son mucho más complejos.
Aquí es donde entra en juego el proyecto Bundler, el administrador de paquetes de Ruby favorito de la comunidad.
El manifiesto Gemfile de Ruby y las dependencias transitivas
Los proyectos de Ruby gestionan sus dependencias de terceros definiéndolas en un archivo Gemfile. Este archivo incluye metadatos del proyecto y también enumera todas las dependencias que se usan para compilarlo.
Este es un ejemplo de un proyecto de Ruby con un Gemfile:
La definición source indica dónde puede encontrar la herramienta bundler estas bibliotecas gem. El repositorio https://rubygems.org es el registro predeterminado y de código abierto de Rubygems.
Luego, cada dependencia se declara en este archivo como una definición de gem, que puede incluir una restricción de versión o no.
Usar Bundler de Ruby para instalar y administrar gemas
A continuación, Bundler, el proyecto de código abierto para administrar gemas, se encarga de instalar y resolver las dependencias. Para empezar a usar Bundler para gestionar las dependencias de Ruby, primero debes tener Ruby y RubyGems instalados en tu entorno. Después, solo tienes que instalar la herramienta bundler de la siguiente manera:
Instalación de dependencias de Ruby y Gemfile.lock
Una vez instalada la herramienta Bundler, puedes instalar las dependencias declaradas en el manifiesto Gemfile con el siguiente comando:
Esto también equivale a ejecutar bundle sin argumentos adicionales, por ejemplo:
Para llevar un registro de las dependencias instaladas, se consulta un archivo Gemfile.lock a fin de reproducir el estado exacto de las versiones de las dependencias en todo el árbol de dependencias anidadas. Si no existe un archivo de este tipo, Bundler instalará las dependencias y luego escribirá el archivo Gemfile.lock.
Ten en cuenta que, en algunos casos, tendrás que especificar la opción --path en bundle install para permitir la creación de archivos.
Una vez ejecutado, el registro de instalación de Bundler se verá así:
Estos son los dos resultados principales de la instalación con Bundler:
Las dependencias de gemas de terceros se instalan en el directorio
vendor/bundle.Se crea el archivo
Gemfile.lockpara registrar el estado exacto de las dependencias en todo el árbol de dependencias anidadas, suponiendo que el archivo no se haya creado antes.
Al inspeccionar el lockfile de dependencias, Gemfile.lock, encontramos la siguiente información:
Inyección en el lockfile Gemfile.lock de RubyGems
La inyección en el lockfile es un vector de ataque en el que un usuario malicioso modifica el archivo Gemfile.lock para que incluya un conjunto de dependencias distinto al del manifiesto Gemfile. Debido a la naturaleza del formato de los lockfiles, que se genera automáticamente, como Gemfile.lock, estos no suelen ser revisados por personas durante las revisiones de código. Para agravar aún más el problema, cuando se realizan muchos cambios de código, la vista de un pull request con los cambios relacionados con este archivo muestra el lockfile contraído.
Para demostrar la gravedad y la frecuencia de este ataque, modificamos el fragmento de código anterior del archivo Gemfile.lock para que incluya un conjunto de dependencias distinto. ¿Puedes encontrar qué paquetes maliciosos se agregaron? ¡No es tan fácil detectarlos como cabría esperar! Planté la gema de Ruby inventada rspec-assertions (~> 2.9.0) como dependencia en el árbol de rspec. Esta dependencia no existe, pero podría existir si creara una gema de Ruby maliciosa para introducirla entre las dependencias que instalas.
El ataque se lleva a cabo modificando el archivo Gemfile.lock y agregando una nueva dependencia anidada dentro del árbol de otra dependencia. Por ejemplo, considera el siguiente contenido del lockfile (solo se muestra el comienzo):
El archivo Gemfile.lock anterior representa una resolución válida del árbol de dependencias. Sin embargo, un usuario malicioso puede actualizar este árbol de dependencias generado por Bundler para agregar la gema legítima llamada digest, de la siguiente manera:
Si ahora ejecutas bundle en la línea de comandos para mantenerte al día con las actualizaciones de paquetes, Bundler instalará la gema digest. Podemos revisar el directorio vendor/ para verificar que se haya agregado la nueva gema:
En este caso, la superficie de ataque permite que desarrolladores desprevenidos, que quizá reciban contribuciones con actualizaciones de paquetes de la comunidad u otras fuentes no verificadas, inyecten nuevas dependencias de gemas maliciosas en Gemfile.lock. Así se envenena el lockfile y se apunta a los responsables del mantenimiento del proyecto.
La superficie de ataque de la inyección en lockfiles de gemas de Ruby
Entonces, ¿qué impacto tiene incluir gemas arbitrarias de Ruby controladas por un atacante en el árbol de dependencias de una aplicación de Ruby?
Como la dependencia de gema recién agregada está creada con fines maliciosos, es poco probable que el código fuente la requiera y que otra dependencia la invoque durante la ejecución. Sin embargo, podríamos aprovechar un vector de ataque similar que existe en el administrador de paquetes npm de JavaScript, que permite ejecutar comandos arbitrarios durante la instalación de dependencias.
Desde 2011, el ecosistema de Ruby tomó una decisión acertada: no permitir que quienes mantienen paquetes se conecten al proceso de instalación de sus dependencias para ejecutar comandos arbitrarios. En un issue de GitHub hay referencias a ese debate, donde explican por qué un script postinstall como el de npm es una mala idea, y con razón.
Dicho esto, las gemas de Ruby permiten incluir código C nativo que se compila durante la instalación. Esta es la oportunidad que buscamos para aprovechar la inyección en el lockfile y ejecutar código arbitrario. De hecho, este método se ha usado en el mundo real desde 2008 y sigue siendo una forma funcional de intervenir en el proceso de instalación de gemas de Ruby.
Creemos una gema de Ruby con fines maliciosos
Empecemos a crear una gema de Ruby en la que agregaremos instrucciones para eludir el mecanismo integrado de la especificación Gemfile, que no permite ejecutar código durante la instalación de forma nativa.
Poner nombres es difícil en el desarrollo de software, pero no para nosotros. Llamaremos a nuestra gema de Ruby woof. Este es el archivo gemspec de nuestra gema de Ruby, llamado woof.gemspec:
Luego, crea el archivo fuente principal de nuestra gema de Ruby: lib/woof.rb:
Después, podemos crear el archivo de extensión nativo de Ruby extconf.rb, que nos permite declarar la información necesaria para crear makefiles, proporcionar instrucciones para extender Ruby con extensiones C nativas y, en general, ejecutar cualquier código de Ruby.
Crea el archivo ext/woof/extconf.rb con el siguiente contenido:
El fragmento de código de extensión anterior es un ejemplo sencillo de cómo crear una extensión C nativa para Ruby. El método create_makefile es un método de Ruby que se usa para crear un makefile para una extensión C nativa. El método create_makefile recibe un único argumento: el nombre de la extensión. El nombre de la extensión debe ser igual al nombre de la gema de Ruby.
La primera línea del código de extensión llama al método File.write, un método de Ruby que escribe un archivo en el sistema de archivos. El primer argumento de File.write es la ruta del archivo que se escribirá y el segundo argumento es el contenido que se escribirá.
En nuestro intento de simular un paquete malicioso (inofensivo), vemos que al instalar nuestra gema de Ruby woof, se crea un archivo nuevo en el sistema de archivos. El archivo se llamará /tmp/woof y estará vacío.
Para que la gema de Ruby se instale correctamente, debemos proporcionar un archivo hello.c válido. Puede ser un programa sencillo de *hola mundo*, como el que aparece en la documentación estándar de Ruby:
Por último, podemos compilar esta gema de Ruby. En el directorio raíz de este proyecto, ejecuta el siguiente comando:
Si todo sale bien, se creará una nueva gema en el directorio raíz del proyecto, llamada woof-1.0.0.gem.
Ahora, para comprobar que esta gema de Ruby funciona según lo previsto, podemos instalarla y verificar que se haya creado un archivo nuevo /tmp/woof (puedes confirmar que no existe antes de instalar el paquete). Sigue estos pasos para instalar la gema de Ruby en una carpeta local:
Crea un directorio temporal nuevo, como
mkdir ~/ruby-gem-test.En este directorio nuevo, crea un directorio llamado
vendor.Indícale a Ruby que use el nuevo directorio
vendorcomo directorio de gemas e instala en él la gemawoofde la siguiente manera:gem install --local /path/to/woof-1.0.0.gem --install-dir 'vendor'
Para confirmar que el ataque de ejecución de código arbitrario se llevó a cabo correctamente como método de instalación de una gema de Ruby, asegúrate de que se haya creado el archivo /tmp/woof.
Cómo prevenir la inyección en lockfiles de gemas de Ruby
Como se muestra a lo largo de este artículo, el proceso de instalación de gemas de Ruby permite que quienes mantienen paquetes ejecuten código arbitrario durante la instalación de sus dependencias. Esta es una superficie de ataque potente que puede combinarse con un ataque de inyección en el lockfile de una aplicación de Ruby y que también es relevante, en términos más generales, para la seguridad de la cadena de suministro en el ecosistema de Ruby.
Lamentablemente, a diferencia del administrador de paquetes npm, que incluye la opción de línea de comandos --ignore-script, la CLI gem de Ruby no permite optar por no compilar extensiones C nativas durante la instalación. Esto deja a los usuarios expuestos a la capacidad de quienes mantienen paquetes de terceros de ejecutar código arbitrario.
Para ayudar a mitigar este tipo de ataque, recomendamos lo siguiente:
Gestiona las dependencias de tus gemas de Ruby y sus actualizaciones mediante un bot automatizado que actualice las versiones cuando sea necesario y te proporcione correcciones de seguridad cuando sea posible. Usa el bot también para gestionar los cambios en el lockfile
Gemfile.lock.Si aceptas contribuciones de desarrolladores a tus archivos
GemfileyGemfile.lock, asegúrate de revisar cuidadosamente estos cambios de código y examinar cada línea modificada.Prioriza la instalación de dependencias con
bundle install --deployment, que calcula el árbol completo de dependencias de todas las gemas de Ruby y detiene la instalación cuandoGemfile.lockno coincide exactamente con las gemas que debería instalarGemfile.
Esta investigación se basa en un trabajo original previo que realicé (Liran Tal) en 2019 para dar a conocer los riesgos de la inyección en lockfiles en el ecosistema de JavaScript.
Más recursos sobre seguridad en Ruby:
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.



