Skip to main content

Ruby-Gem-Installationen können Sie Lockfile-Injection-Angriffen aussetzen

Artikel von
blog hero ruby gem lockfile injection

17. August 2022

0 Min. Lesezeit

In diesem Beitrag betrachten wir die Sicherheitslücken durch Lockfile-Injection, denen ein Ruby-Gem Sie über seine Gemfile.lock aussetzen kann. Zunächst geben wir eine kurze Einführung in Ruby und die Verwaltung von Drittanbieterabhängigkeiten mit RubyGems und Bundler.

Webentwickler arbeiten häufig an Ruby-Projekten, meinen damit aber meist das beliebte Open-Source-Webanwendungsframework Ruby on Rails. Ruby selbst ist die zugrunde liegende dynamische Open-Source-Sprache, auf der das Projekt basiert.

Damit Ruby-Code modular und wiederverwendbar ist, wird er mit dem Projekt RubyGems paketiert, das häufig über die Befehlszeile mit der ausführbaren Datei gem bedient wird. Diese Pakete werden als Gems bezeichnet und in einer Paketmanifestdatei namens Gemfile aufgeführt.

RubyGems ist inzwischen in die Ruby-Sprache integriert und muss nicht separat installiert werden. Entwickler können ein Gem ganz einfach mit gem install <gem-name> installieren. Die umfassende Verwaltung transitiver Abhängigkeiten, das Festlegen von Versionen und die Versionierung sind jedoch deutlich komplexer.

Hier kommt das Projekt Bundler ins Spiel – der beliebteste Ruby-Paketmanager der Community.

Das Gemfile-Paketmanifest von Ruby und transitive Abhängigkeiten

Ruby-Projekte verwalten ihre Drittanbieterabhängigkeiten, indem sie eine Datei namens Gemfile definieren. Diese Datei enthält Projektmetadaten und führt außerdem alle Abhängigkeiten auf, die zum Erstellen des Projekts verwendet werden.

Hier sehen Sie ein Beispiel für ein Ruby-Projekt mit einem Gemfile:

# Gemfile

source 'https://rubygems.org'

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

Die Definition source gibt an, wo das Tool bundler diese Gem-Bibliotheken findet. Das Repository unter https://rubygems.org ist die Standard- und Open-Source-Rubygems-Registry.

Jede Abhängigkeit wird in dieser Datei als Gem-Definition aufgeführt, optional mit einer Versionsbeschränkung.

Gems mit Rubys Bundler installieren und verwalten

Bundler – das Open-Source-Projekt zur Gem-Verwaltung – ist für die Installation und Auflösung von Abhängigkeiten zuständig. Um mit Bundler die Abhängigkeiten Ihres Ruby-Projekts zu verwalten, müssen zunächst Ruby und RubyGems in Ihrer Umgebung installiert sein. Anschließend installieren Sie einfach das Tool bundler wie folgt:

gem install bundler

Ruby-Abhängigkeiten und Gemfile.lock installieren

Sobald das Tool Bundler installiert ist, können Sie die im Manifest Gemfile aufgeführten Abhängigkeiten mit folgendem Befehl installieren:

bundle install

Das entspricht auch der Ausführung von bundle ohne zusätzliche Argumente, zum Beispiel:

bundle

Um die installierten Abhängigkeiten nachzuverfolgen, wird eine Datei namens Gemfile.lock herangezogen. Sie ermöglicht es, den exakten Stand der Abhängigkeitsversionen im gesamten Baum verschachtelter Abhängigkeiten wiederherzustellen. Existiert diese Datei nicht, installiert Bundler die Abhängigkeiten und erstellt anschließend die Datei Gemfile.lock.

Beachten Sie, dass Sie in manchen Fällen die Option --path für bundle install angeben müssen, damit Dateien erstellt werden können.

Nach der Ausführung sieht ein Bundler-Installationsprotokoll etwa so aus:

$ 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`

Die Installation mit Bundler führt hauptsächlich zu zwei Ergebnissen:

  1. Drittanbieter-Gem-Abhängigkeiten werden im Verzeichnis vendor/bundle installiert.

  2. Die Datei Gemfile.lock wird erstellt, um den exakten Stand der Abhängigkeiten im gesamten Baum verschachtelter Abhängigkeiten festzuhalten – sofern sie nicht bereits vorhanden ist.

Wenn wir die Abhängigkeitssperrdatei Gemfile.lock untersuchen, sehen wir folgende Informationen:

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

Lockfile-Injection in RubyGems über Gemfile.lock

Lockfile-Injection ist ein Angriffsvektor, bei dem ein böswilliger Nutzer die Datei Gemfile.lock so verändert, dass sie andere Abhängigkeiten enthält als das Manifest Gemfile. Da Lockfiles wie Gemfile.lock maschinell generiert werden, werden sie bei Code-Reviews häufig nicht von Menschen geprüft. Hinzu kommt, dass in der Pull-Request-Ansicht die Änderungen am Lockfile eingeklappt dargestellt werden, wenn umfangreiche Codeänderungen vorgenommen wurden.

Um die Schwere und Verbreitung dieses Angriffs zu veranschaulichen, wurde das oben gezeigte Codebeispiel einer Gemfile.lock geändert und mit anderen Abhängigkeiten versehen. Können Sie erkennen, welche bösartigen Pakete hinzugefügt wurden? Das ist schwieriger, als Sie vielleicht denken! Ich habe das erfundene Ruby-Gem rspec-assertions (~> 2.9.0) als Abhängigkeit in den rspec-Baum eingefügt. Diese Abhängigkeit existiert überhaupt nicht. Sie könnte aber existieren, wenn ich ein bösartiges Ruby-Gem erstellte, um es in die von Ihnen installierten Abhängigkeiten einzuschleusen.

Für den Angriff wird die Datei Gemfile.lock geändert und eine neue verschachtelte Abhängigkeit in den Baum einer anderen Abhängigkeit eingefügt. Betrachten Sie beispielsweise den folgenden Inhalt eines Lockfiles (hier ist nur der Anfang zu sehen):

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

Die oben gezeigte Gemfile.lock enthält eine gültige Auflösung des Abhängigkeitsbaums. Ein böswilliger Nutzer kann diesen von Bundler generierten Abhängigkeitsbaum jedoch so ändern, dass das legitime Gem digest wie folgt hinzugefügt wird:

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)

Wenn Sie nun bundle an der Befehlszeile ausführen, um Paketupdates zu übernehmen, installiert Bundler das Gem digest. Im Verzeichnis vendor/ können wir das neue Gem überprüfen:

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

Dieser Angriffsvektor ermöglicht es, dass arglose Entwickler, die Paketupdate-Beiträge aus der Community oder anderen ungeprüften Quellen erhalten, neue bösartige Gem-Abhängigkeiten in die Gemfile.lock einschleusen. So wird das Lockfile kompromittiert und gezielt Projektbetreibern geschadet.

Angriffsfläche für Lockfile-Injection bei Ruby-Gems

Welche Auswirkungen hat es also, wenn beliebige, von einem Angreifer kontrollierte Ruby-Gems Teil des Abhängigkeitsbaums einer Ruby-Anwendung werden?

Da die neu hinzugefügte Gem-Abhängigkeit eigens für böswillige Zwecke erstellt wurde, wird sie wahrscheinlich weder vom Quellcode benötigt noch zur Laufzeit von einer anderen Abhängigkeit aufgerufen. Wir könnten jedoch einen ähnlichen Angriffsvektor wie im JavaScript-Paketmanager npm ausnutzen, bei dem beliebige Befehle während der Installation von Abhängigkeiten ausgeführt werden können.

Schon 2011 traf das Ruby-Ökosystem die kluge Entscheidung, Paketbetreibern nicht zu erlauben, sich in den Installationsprozess ihrer Abhängigkeiten einzuklinken, um beliebige Befehle auszuführen. In einem GitHub-Issue finden Sie Verweise auf die Diskussion, in der erläutert wird, warum ein npm-ähnliches postinstall-Skript eine schlechte Idee ist – und das zu Recht.

Allerdings erlauben Ruby-Gems, nativen C-Code einzubinden, der während der Installation kompiliert wird. Genau diese Möglichkeit lässt sich für die Lockfile-Injection und die Ausführung beliebigen Codes ausnutzen. Tatsächlich wird diese Methode seit 2008 in freier Wildbahn eingesetzt und funktioniert weiterhin, um sich in den Installationsprozess von Ruby-Gems einzuklinken.

Erstellen wir ein Ruby-Gem für böswillige Zwecke

Beginnen wir mit dem Erstellen eines Ruby-Gems, das Anweisungen enthält, um den integrierten Mechanismus der Gemfile-Spezifikation zu umgehen, der die Codeausführung während der Installation standardmäßig nicht zulässt.

Das Benennen von Dingen ist in der Softwareentwicklung schwierig – für uns aber nicht. Unser Ruby-Gem soll woof heißen. Hier sehen Sie die gemspec-Datei unseres Ruby-Gems namens 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

Erstellen Sie anschließend die primäre Quelldatei für unser Ruby-Gem: lib/woof.rb:

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

Anschließend erstellen wir Rubys native Erweiterungsdatei extconf.rb. Mit ihr können wir die für Makefiles erforderlichen Informationen angeben, Anweisungen zur Erweiterung von Ruby mit nativen C-Erweiterungen bereitstellen und allgemein beliebigen Ruby-Code ausführen.

Erstellen Sie die Datei ext/woof/extconf.rb mit folgendem Inhalt:

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

require 'mkmf'
create_makefile('hello_c')

Das obige Codebeispiel für die Erweiterung zeigt auf einfache Weise, wie sich eine native C-Erweiterung für Ruby erstellen lässt. Die Methode create_makefile ist eine Ruby-Methode, mit der ein Makefile für eine native C-Erweiterung erstellt wird. Die Methode create_makefile erwartet ein einzelnes Argument: den Namen der Erweiterung. Dieser sollte mit dem Namen des Ruby-Gems übereinstimmen.

Die erste Zeile des Erweiterungscodes ruft die Methode File.write auf. Dabei handelt es sich um eine Ruby-Methode, die eine Datei in das Dateisystem schreibt. Das erste Argument für File.write ist der Pfad der zu schreibenden Datei, das zweite Argument ist der Inhalt, der in die Datei geschrieben werden soll.

Bei unserem Versuch, ein (harmloses) bösartiges Paket zu simulieren, sehen wir, dass bei der Installation unseres Ruby-Gems woof eine neue Datei im Dateisystem erstellt wird. Sie heißt /tmp/woof und ist leer.

Damit sich das Ruby-Gem erfolgreich installieren lässt, benötigen wir eine gültige Datei hello.c. Dafür genügt ein einfaches *Hello-World*-Programm aus der Ruby-Dokumentation:

#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);
}

Zum Schluss können wir dieses Ruby-Gem erstellen. Führen Sie im Stammverzeichnis dieses Projekts den folgenden Befehl aus:

gem build woof.gemspec

Nach erfolgreicher Ausführung wird im Stammverzeichnis des Projekts ein neues Gem mit dem Namen woof-1.0.0.gem erstellt.

Um zu prüfen, ob dieses Ruby-Gem wie erwartet funktioniert, können wir es installieren und feststellen, ob die neue Datei /tmp/woof erstellt wurde (Sie können vor der Installation überprüfen, ob sie noch nicht existiert). Gehen Sie wie folgt vor, um das Ruby-Gem in einem lokalen Ordner zu installieren:

  1. Erstellen Sie ein neues temporäres Verzeichnis, zum Beispiel mit mkdir ~/ruby-gem-test.

  2. Erstellen Sie in diesem neuen Verzeichnis ein Verzeichnis namens vendor.

  3. Weisen Sie Ruby an, das neue Verzeichnis vendor als Gem-Verzeichnis zu verwenden, und installieren Sie das Gem woof wie folgt darin: gem install --local /path/to/woof-1.0.0.gem --install-dir 'vendor'

Um zu bestätigen, dass unser Angriff zur Ausführung beliebigen Codes als Ruby-Gem-Installationsmethode erfolgreich war, prüfen Sie, ob eine neue Datei /tmp/woof erstellt wurde.

Lockfile-Injection bei Ruby-Gems verhindern

Wie dieser Artikel zeigt, können Paketbetreuer während der Installation ihrer Abhängigkeiten beliebigen Code ausführen. Das ist eine mächtige Angriffsfläche, die in Kombination mit Lockfile-Injection in Ruby-Anwendungen ausgenutzt werden kann – und ganz allgemein auch die Supply-Chain-Sicherheit im Ruby-Ökosystem betrifft.

Leider bietet die eigene gem-CLI von Rub – anders als der npm-Paketmanager mit seinem Befehlszeilen-Flag --ignore-script – keine Möglichkeit, die Kompilierung nativer C-Erweiterungen während der Installation zu überspringen. Dadurch können Drittanbieter-Paketbetreuer beliebigen Code ausführen und Nutzer bleiben gefährdet.

Um diese Art von Angriff einzudämmen, empfehlen wir Folgendes:

  1. Verwalten Sie Ihre Ruby-Gem-Abhängigkeiten und deren Updates ausschließlich mit einem automatisierten Bot. Dieser kann Ihre Ruby-Gem-Versionen bei Bedarf aktualisieren und Ihnen, sofern möglich, Sicherheitskorrekturen bereitstellen. Verwenden Sie ihn auch für Änderungen am Lockfile Gemfile.lock.

  2. Wenn Sie Beiträge von Entwicklern zu Ihren Dateien Gemfile und Gemfile.lock annehmen, prüfen Sie diese Codeänderungen sorgfältig und untersuchen Sie jede einzelne geänderte Zeile genau.

  3. Installieren Sie Abhängigkeiten vorzugsweise mit bundle install --deployment. Damit wird der vollständige Abhängigkeitsbaum aller Ruby-Gems berechnet und der Installationsprozess angehalten, wenn die Gemfile.lock nicht genau den Gems entspricht, die gemäß der Gemfile installiert werden sollten.

Diese Recherche basiert auf meiner früheren Originalarbeit aus dem Jahr 2019 (Liran Tal) zur Offenlegung der Risiken von Lockfile-Injection im JavaScript-Ökosystem.

Weitere Ressourcen zur Ruby-Sicherheit:

Starten Sie mit Capture-the-Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Challenges lösen.

Weiterlesen

blog feature snyk container custom base recommendations
Blog

Agentic AppSec: Softwareentwicklung ist agentisch geworden. Security nicht.

KI-gestützte Entwicklung überholt herkömmliche Security-Workflows. Snyk Agentic AppSec setzt Security-Agenten ein, die Schwachstellen kontinuierlich finden, beheben und überprüfen – während Menschen die Maßstäbe festlegen.

blog feature ai violet
Blog

VulnBump: Können KI-Agenten anfällige Abhängigkeiten beheben, ohne den Build zu beschädigen?

VulnBump testet KI-Agenten anhand von 307 realen Dependency-Upgrades. Eine Lösung zählt nur, wenn die Sicherheitslücke behoben ist und Build sowie Tests weiterhin erfolgreich sind.

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.