Skip to main content

L’installation de gems Ruby peut vous exposer aux attaques par injection de fichiers lock

Écrit par
blog hero ruby gem lockfile injection

17 août 2022

0 minutes de lecture

Dans cet article, nous examinons les angles morts de sécurité liés à l’injection de fichiers lock à laquelle une gem Ruby peut vous exposer via son fichier Gemfile.lock. En préambule, nous présenterons brièvement Ruby et la gestion des dépendances tierces avec RubyGems et Bundler.

Les développeurs web travaillent souvent sur des projets Ruby, mais les désignent généralement par le nom du framework open source populaire de développement d’applications web Ruby on Rails. Ruby est le langage dynamique open source sous-jacent qui fait fonctionner ce projet.

Pour rendre le code Ruby modulaire et réutilisable, on le regroupe à l’aide du projet RubyGems, avec lequel on interagit souvent via l’interface en ligne de commande, au moyen de l’exécutable gem. Ces packages sont appelés des gems et sont référencés dans un fichier manifeste de package appelé Gemfile.

RubyGems est désormais fourni avec le langage Ruby et n’a pas besoin d’être installé séparément. Un développeur peut installer une gem avec une commande aussi simple que gem install <gem-name>, mais la gestion globale des dépendances transitives, le verrouillage des versions et leur gestion sont bien plus complexes.

C’est là qu’intervient le projet Bundler, le gestionnaire de packages Ruby préféré de la communauté.

Manifeste Gemfile et dépendances transitives de Ruby

Les projets Ruby gèrent leurs dépendances tierces en définissant un fichier Gemfile. Ce fichier contient les métadonnées du projet et répertorie également toutes les dépendances utilisées pour le construire.

Voici un exemple de projet Ruby avec un Gemfile :

# Gemfile

source 'https://rubygems.org'

gem 'rails', '~> 5.2.0'
gem 'nokogiri', '~> 1.6.1'
gem 'rspec'

La définition source indique où l’outil bundler peut trouver ces bibliothèques gem. Le dépôt https://rubygems.org est le registre Rubygems par défaut, qui est open source.

Chaque dépendance est ensuite déclarée dans ce fichier sous la forme d’une définition de gem, avec ou sans contrainte de version.

Utiliser Bundler pour installer et gérer les gems Ruby

Bundler, le projet open source de gestion des gems, est chargé d’installer et de résoudre les dépendances. Pour commencer à utiliser Bundler afin de gérer vos dépendances Ruby, vous devez d’abord installer Ruby et RubyGems dans votre environnement. Il vous suffit ensuite d’installer l’outil bundler comme suit :

gem install bundler

Installation des dépendances Ruby et fichier Gemfile.lock

Une fois l’outil bundler installé, vous pouvez installer les dépendances déclarées dans le manifeste Gemfile en exécutant la commande suivante :

bundle install

Cela revient également à exécuter bundle sans argument supplémentaire, comme ceci :

bundle

Pour suivre les dépendances installées, un fichier Gemfile.lock est consulté afin de recréer exactement les versions des dépendances dans l’arborescence des dépendances imbriquées. Si ce fichier n’existe pas, bundler installe les dépendances, puis crée le fichier Gemfile.lock.

Notez que dans certains cas, vous devrez préciser l’option --path à la commande bundle install pour permettre la création de fichiers.

Après son exécution, le journal d’installation de bundler se présente comme suit :

$ bundle install --path vendor/bundle

Fetching gem metadata from https://rubygems.org/...........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Fetching rake 13.0.6
Installing rake 13.0.6
Fetching concurrent-ruby 1.1.10
Installing concurrent-ruby 1.1.10
Fetching i18n 1.12.0
Installing i18n 1.12.0
Fetching minitest 5.16.2
Installing minitest 5.16.2

[... lines were removed for brevity ...]

Fetching rspec 3.11.0
Installing rspec 3.11.0
Bundle complete! 3 Gemfile dependencies, 47 gems now installed.
Bundled gems are installed into `./vendor/bundle`

L’installation avec bundler produit principalement les deux résultats suivants :

  1. Les dépendances de gems tierces sont installées dans le répertoire vendor/bundle.

  2. Le fichier Gemfile.lock est créé pour consigner précisément l’état des dépendances dans l’arborescence des dépendances imbriquées, s’il n’existait pas déjà.

En examinant le fichier de verrouillage des dépendances, Gemfile.lock, on trouve les informations suivantes :

GEM
  remote: https://rubygems.org/
  specs:
    actioncable (5.2.8.1)
      actionpack (= 5.2.8.1)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
    actionmailer (5.2.8.1)
      actionpack (= 5.2.8.1)
      actionview (= 5.2.8.1)
      activejob (= 5.2.8.1)
      mail (~> 2.5, >= 2.5.4)
      rails-dom-testing (~> 2.0)

    [... lines were removed for brevity ...]

    rails-dom-testing (2.0.3)
      activesupport (>= 4.2.0)
      nokogiri (>= 1.6)
    rails-html-sanitizer (1.4.3)
      loofah (~> 2.3)
    railties (5.2.8.1)
      actionpack (= 5.2.8.1)
      activesupport (= 5.2.8.1)
      method_source
      rake (>= 0.8.7)
      thor (>= 0.19.0, < 2.0)
    rspec (3.11.0)
      rspec-core (~> 3.11.0)
      rspec-expectations (~> 3.11.0)
      rspec-mocks (~> 3.11.0)
      rspec-assertions (~> 2.9.0)
    rspec-core (3.11.0)
      rspec-support (~> 3.11.0)
    rspec-expectations (3.11.0)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.11.0)
    rspec-mocks (3.11.1)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.11.0)
    rspec-support (3.11.0)

PLATFORMS
  ruby

DEPENDENCIES
  nokogiri
  rails (~> 5.2.0)
  rspec

BUNDLED WITH
   1.17.2

Injection dans le fichier lock Gemfile.lock de RubyGems

L’injection de fichier lock est un vecteur d’attaque dans lequel un utilisateur malveillant modifie le fichier Gemfile.lock pour qu’il contienne un ensemble de dépendances différent de celui du manifeste Gemfile. En raison du format généré automatiquement des fichiers lock, comme Gemfile.lock, ceux-ci sont rarement examinés par des humains lors des revues de code. Pour aggraver encore le problème, lorsque d’importantes modifications de code ont été apportées, la vue d’une pull request présentant les changements apportés au fichier affiche le fichier lock de manière réduite.

Pour illustrer la gravité et la fréquence de cette attaque, l’extrait de code ci-dessus représentant un fichier Gemfile.lock a été modifié pour contenir un ensemble de dépendances différent. Pouvez-vous repérer les packages malveillants qui ont été ajoutés ? Ce n’est pas aussi facile qu’on pourrait le croire ! J’ai ajouté la fausse gem Ruby rspec-assertions (~> 2.9.0) comme dépendance dans l’arborescence de rspec. Cette dépendance n’existe pas, mais pourrait exister si je créais une gem Ruby malveillante pour l’intégrer aux dépendances que vous installez.

L’attaque consiste à modifier le fichier Gemfile.lock et à ajouter une nouvelle dépendance imbriquée dans l’arborescence d’une autre dépendance. Prenons, par exemple, le contenu suivant d’un fichier lock (seul le début est affiché) :

GEM
  remote: https://rubygems.org/
  specs:
    actioncable (5.2.8.1)
      actionpack (= 5.2.8.1)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)

Le fichier Gemfile.lock ci-dessus représente une résolution valide de l’arborescence des dépendances. Toutefois, un utilisateur malveillant peut modifier cette arborescence générée par bundler pour y ajouter la gem légitime appelée digest, comme suit :

GEM
  remote: https://rubygems.org/
  specs:
    actioncable (5.2.8.1)
      actionpack (= 5.2.8.1)
      digest (3.1.0)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)

Si vous exécutiez maintenant bundle dans l’invite de commande pour maintenir vos packages à jour, bundler installerait la gem digest. Nous pouvons examiner le répertoire vendor/ pour vérifier la présence de la nouvelle gem :

ls -alh vendor/bundle/ruby/2.6.0/gems/digest-3.1.0            
total 16
drwxr-xr-x   6 lirantal  staff   192B Jul 18 01:24 .
drwxr-xr-x  49 lirantal  staff   1.5K Jul 18 01:24 ..
-rw-r--r--   1 lirantal  staff   1.3K Jul 18 01:24 LICENSE.txt
-rw-r--r--   1 lirantal  staff   3.0K Jul 18 01:24 README.md
drwxr-xr-x   3 lirantal  staff    96B Jul 18 01:24 ext
drwxr-xr-x   5 lirantal  staff   160B Jul 18 01:24 lib

Dans ce cas, la surface d’attaque permet à des développeurs peu méfiants, qui peuvent recevoir des contributions de mise à jour de packages de la communauté ou d’autres sources non vérifiées, d’injecter de nouvelles dépendances de gems malveillantes dans le fichier Gemfile.lock, empoisonnant ainsi le fichier lock et ciblant les responsables du projet.

Surface d’attaque liée à l’injection de fichiers lock dans les gems Ruby

Quel est donc l’impact de l’ajout de gems Ruby arbitraires contrôlées par un attaquant à l’arborescence des dépendances d’une application Ruby ?

Comme la nouvelle dépendance gem a été conçue à des fins malveillantes, il est peu probable que le code source en ait besoin et qu’une autre dépendance l’appelle à l’exécution. Mais nous pourrions exploiter un vecteur d’attaque similaire dans le gestionnaire de packages npm de JavaScript, qui permet d’exécuter des commandes arbitraires lors de l’installation des dépendances.

Dès 2011, l’écosystème Ruby a pris une décision judicieuse : ne pas permettre aux responsables de packages de s’insérer dans le processus d’installation des dépendances pour exécuter des commandes arbitraires. Vous trouverez les références à cette discussion dans une issue GitHub, où les arguments contre un script postinstall à la npm sont exposés, à juste titre.

Cela dit, les gems Ruby peuvent inclure du code C natif qui sera compilé lors de l’installation. C’est la faille que nous cherchons à exploiter pour exécuter du code arbitraire par injection de fichier lock. En fait, cette méthode est utilisée dans la nature depuis 2008 et permet toujours de s’insérer dans le processus d’installation des gems Ruby.

Créons une gem Ruby à des fins malveillantes

Commençons par créer une gem Ruby dans laquelle nous ajouterons des instructions permettant de contourner le mécanisme intégré de la spécification Gemfile, qui ne permet pas nativement l’exécution de code pendant l’installation.

Il est difficile de trouver un nom en programmation, mais pas pour nous. Nous appellerons notre gem Ruby woof. Voici le fichier gemspec de notre gem Ruby, nommé woof.gemspec :

Gem::Specification.new do |s|
    s.name        = "woof"
    s.version     = "1.0.0"
    s.summary     = "Woof"
    s.description = "A hello world gem, used for testing"
    s.authors     = ["Liran Tal"]
    s.email       = "liran@snyk.io"
    s.files       = ["lib/woof.rb"]
    s.homepage    =
      "https://rubygems.org/gems/woof"
    s.license       = "MIT"
    s.extensions << 'ext/woof/extconf.rb'
  end

Créez ensuite le fichier source principal de notre gem Ruby, lib/woof.rb :

class Woof
    def self.hi
      puts "woof woof"
    end
  end

Nous pouvons ensuite créer le fichier d’extension natif de Ruby, extconf.rb, qui nous permet de déclarer les informations nécessaires à la création de makefiles, de fournir des instructions pour étendre Ruby avec des extensions C natives et, de manière générale, d’exécuter n’importe quel code Ruby.

Créez le fichier ext/woof/extconf.rb avec le contenu suivant :

# Use the Ruby languague API to create an empty file during install /tmp/woof
File.write("/tmp/woof", "")

require 'mkmf'
create_makefile('hello_c')

L’extrait de code d’extension ci-dessus est un exemple simple de création d’une extension C native pour Ruby. La méthode create_makefile est une méthode Ruby utilisée pour créer un makefile pour une extension C native. La méthode create_makefile prend un seul argument : le nom de l’extension. Celui-ci doit être identique au nom de la gem Ruby.

La toute première ligne du code de l’extension appelle la méthode File.write, une méthode Ruby qui écrit un fichier dans le système de fichiers. Le premier argument de File.write est le chemin du fichier à écrire, et le second est le contenu à y écrire.

Pour simuler un package malveillant (inoffensif), nous constatons qu’à l’installation de notre gem Ruby woof, un nouveau fichier est créé dans le système de fichiers. Il s’appelle /tmp/woof et est vide.

Pour que la gem Ruby s’installe correctement, nous devons fournir un fichier hello.c valide. Il peut s’agir d’un simple programme *hello world* tiré de la documentation Ruby standard, comme suit :

#include <stdio.h>
#include "ruby.h"

VALUE world(VALUE self) {
  printf("Hello World!\n");
  return Qnil;
}

// The initialization method for this module
void Init_hello_c() {
  printf("Hello World!\n");
  VALUE HelloC = rb_define_module("HelloC");
  rb_define_singleton_method(HelloC, "world", world, 0);
}

Enfin, nous pouvons créer cette gem Ruby. Dans le répertoire racine de ce projet, exécutez la commande suivante :

gem build woof.gemspec

Une fois l’opération réussie, une nouvelle gem nommée woof-1.0.0.gem est créée dans le répertoire racine du projet.

Pour vérifier que cette gem Ruby fonctionne comme prévu, nous pouvons l’installer et constater la création d’un nouveau fichier /tmp/woof (vous pouvez vérifier qu’il n’existe pas avant l’installation du package). Procédez comme suit pour installer la gem Ruby dans un dossier local :

  1. Créez un nouveau répertoire temporaire, par exemple mkdir ~/ruby-gem-test.

  2. Dans ce nouveau répertoire, créez un dossier nommé vendor.

  3. Indiquez à Ruby d’utiliser le nouveau répertoire vendor comme répertoire des gems et d’y installer la gem woof, comme suit : gem install --local /path/to/woof-1.0.0.gem --install-dir 'vendor'

Pour confirmer que notre attaque par exécution de code arbitraire a bien été menée à bien comme méthode d’installation d’une gem Ruby, vérifiez qu’un nouveau fichier /tmp/woof a été créé.

Prévenir les attaques par injection de fichiers lock dans les gems Ruby

Comme le montre cet article, le processus d’installation des gems Ruby permet aux responsables de packages d’exécuter du code arbitraire lors de l’installation de leurs dépendances. Il s’agit d’une surface d’attaque importante, qui peut être exploitée conjointement à l’injection de fichiers lock dans une application Ruby, mais aussi plus généralement dans le domaine de la sécurité de la chaîne d’approvisionnement de l’écosystème Ruby.

Malheureusement, contrairement au gestionnaire de packages npm, qui propose l’option de ligne de commande --ignore-script, la CLI gem de Ruby ne permet pas de désactiver la compilation des extensions C natives pendant l’installation. Les utilisateurs restent donc exposés à la possibilité que des responsables de packages tiers exécutent du code arbitraire.

Pour contribuer à atténuer ce type d’attaque, nous vous recommandons de suivre ces conseils :

  1. Gérez les dépendances de vos gems Ruby et leurs mises à jour, ainsi que les modifications du fichier lock Gemfile.lock, à l’aide d’un bot automatisé. Celui-ci mettra à niveau les versions de vos gems Ruby selon les besoins et vous fournira, dans la mesure du possible, des correctifs de sécurité.

  2. Si vous acceptez des contributions de développeurs à vos fichiers Gemfile et Gemfile.lock, veillez à examiner attentivement ces modifications de code et à passer chaque ligne modifiée au crible.

  3. Privilégiez l’installation des dépendances avec bundle install --deployment. Cette commande calcule l’arborescence complète des dépendances de toutes les gems Ruby et interrompt l’installation si le fichier Gemfile.lock ne correspond pas exactement aux gems à installer d’après le fichier Gemfile.

Cette recherche s’appuie sur des travaux originaux antérieurs que j’ai menés (Liran Tal) en 2019 pour révéler les risques d’injection de fichiers lock dans l’écosystème JavaScript.

Autres ressources sur la sécurité Ruby :

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.

Lire la suite

blog feature snyk container custom base recommendations
Blog

AppSec agentique : le développement logiciel est devenu agentique. Pas la sécurité.

Le développement piloté par l’IA dépasse les workflows de sécurité traditionnels. L’AppSec agentique de Snyk s’appuie sur des agents de sécurité pour détecter, corriger et vérifier les vulnérabilités en continu, tandis que les équipes définissent les exigences.

blog feature ai violet
Blog

VulnBump : les agents IA peuvent-ils corriger les dépendances vulnérables sans casser le build ?

VulnBump évalue des agents IA sur 307 mises à niveau de dépendances réelles. Une correction n’est comptabilisée que si la vulnérabilité disparaît et que la compilation et les tests restent au vert.

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.