Ruby gemのインストールでlockfile injection攻撃にさらされる可能性
2022年8月17日
0 分で読めますこの記事では、Ruby gemがGemfile.lockを介して引き起こす可能性のある、lockfile injectionのセキュリティ上の盲点を見ていきます。その前に、Rubyと、RubyGemsおよびBundlerを使ったサードパーティ依存関係の管理について簡単に説明します。
Web開発者はRubyプロジェクトに取り組むことがよくありますが、それらを人気のオープンソースWebアプリケーションフレームワークRuby on Railsと呼ぶことがほとんどです。Rubyは、プロジェクトを支える基盤となるオープンソースの動的言語です。
Rubyコードをモジュール化して再利用できるようにするため、RubyGemsプロジェクトを使ってパッケージ化します。多くの場合、コマンドラインインターフェースからgem実行ファイルを介して操作します。これらのパッケージはgemと呼ばれ、Gemfileというパッケージマニフェストファイルで参照されます。
RubyGemsは現在、Ruby言語自体に同梱されているため、別途インストールする必要はありません。gem install <gem-name>のように簡単にgemをインストールすることもできますが、推移的依存関係全体の管理、バージョンの固定や指定は、はるかに複雑です。
そこで登場するのが、コミュニティで愛用されているRubyパッケージマネージャー、Bundlerプロジェクトです。
RubyのGemfileパッケージマニフェストと推移的依存関係
Rubyプロジェクトでは、Gemfileファイルを定義してサードパーティ依存関係を管理します。このファイルにはプロジェクトのメタデータに加え、プロジェクトのビルドに使う依存関係がすべて記載されています。
Gemfileを使ったRubyプロジェクトの例を見てみましょう。
sourceの定義では、bundlerツールがgemライブラリを検索する場所を指定します。https://rubygems.orgのリポジトリは、デフォルトのオープンソースRubygemsレジストリです。
各依存関係は、このファイルでgem定義として宣言します。バージョン制約を含めることも、指定しないこともできます。
Rubyのbundlerを使ったgemのインストールと管理
オープンソースのgem管理プロジェクトであるBundlerが、依存関係のインストールと解決を担います。bundlerでRubyの依存関係を管理するには、まず環境にRubyとRubyGemsの両方をインストールしておく必要があります。その後、次のようにbundlerツールをインストールするだけで準備完了です。
Rubyの依存関係とGemfile.lockのインストール
bundlerツールをインストールしたら、Gemfileマニフェストに宣言した依存関係を、次のコマンドを実行してインストールできます。
これは、次のように追加の引数なしでbundleを実行するのと同じです。
インストール済みの依存関係を追跡するため、Gemfile.lockファイルを参照して、依存関係ツリー全体における各バージョンの状態を正確に再現します。このファイルが存在しない場合、bundlerは依存関係をインストールしてからGemfile.lockファイルを書き込みます。
場合によっては、ファイルを作成できるようにbundle installに--pathオプションを指定する必要があります。
実行すると、bundlerのインストールログは次のようになります。
bundlerのインストールによって、主に次の2つの結果が得られます。
サードパーティgemの依存関係が
vendor/bundleディレクトリにインストールされます。依存関係ツリー全体における依存関係の正確な状態を記録するために、
Gemfile.lockファイルが作成されます(このファイルがまだ作成されていない場合)。
依存関係のlockfileであるGemfile.lockを確認すると、次の情報が記載されています。
RubyGemsのGemfile.lockへのlockfile injection
lockfile injectionとは、悪意あるユーザーがGemfile.lockファイルを改変し、Gemfileマニフェストとは異なる依存関係を含める攻撃手法です。Gemfile.lockのようなlockfileは機械生成の形式であるため、コードレビューで人の目による確認を受けないことが少なくありません。さらに、大量のコード変更がある場合、ファイルに関するプルリクエストの差分表示ではlockfileが折りたたまれて表示されるため、問題が見過ごされやすくなります。
この攻撃の深刻さと発生しやすさを示すため、先ほどのGemfile.lockのコード例を改変し、異なる依存関係を含めました。追加された悪意あるパッケージを見つけられますか?思ったほど簡単には見つかりません。rspecツリーの依存関係として、架空のRuby gemであるrspec-assertions (~> 2.9.0)を仕込みました。この依存関係は実際には存在しませんが、インストールする依存関係に紛れ込ませる悪意あるRuby gemを作れば、実現できてしまいます。
この攻撃では、Gemfile.lockファイルを改変し、別の依存関係のツリーに新たなネストされた依存関係を追加します。たとえば、次のlockfileの内容(冒頭部分のみ)を見てみましょう。
上記のGemfile.lockは、有効な依存関係ツリーの解決結果です。しかし、悪意あるユーザーはbundlerが生成したこの依存関係ツリーを更新し、次のように正規のgemであるdigestを追加できます。
パッケージの更新を取り込むために、コマンドラインでbundleを実行すると、bundlerはdigest gemをインストールします。vendor/ディレクトリをさらに確認して、新しいgemが追加されたことを確かめられます。
この攻撃対象領域を悪用すると、コミュニティや未検証のソースからパッケージ更新の貢献を受け取った開発者が、悪意ある新しいgemの依存関係をGemfile.lockに注入できます。つまりlockfileを汚染し、プロジェクトのメンテナーを標的にできるのです。
Ruby gemsへのlockfile injection攻撃の対象領域
では、攻撃者が制御する任意のRuby gemが、Rubyアプリケーションの依存関係ツリーに含まれると、どのような影響があるのでしょうか?
新たに追加されたgemの依存関係は悪意ある目的で作られているため、ソースコードから要求され、別の依存関係によって実行時に呼び出される可能性は高くありません。しかし、依存関係のインストール時に任意のコマンドを実行できる、JavaScriptのnpmパッケージマネージャーに存在するものと同様の攻撃手法を悪用できる可能性があります。
2011年までさかのぼると、Rubyエコシステムでは、パッケージメンテナーが依存関係のインストールプロセスに処理を組み込み、任意のコマンドを実行できないようにする賢明な判断がなされました。npmのようなpostinstallスクリプトがなぜ悪い考えなのかを議論したGitHub issueを参照してください。
一方、Ruby gemでは、インストール時にコンパイルされるネイティブCコードをバンドルできます。これが、lockfile injectionを利用した任意コード実行を可能にする突破口です。実際、この手法は2008年から実環境で使われており、現在もRubyのgemインストールプロセスに処理を組み込む方法として有効です。
悪意ある目的でRuby gemを作成する
それでは、インストール時のコード実行を標準では許可しないGemfileの仕様を回避する命令を追加したRuby gemを作成してみましょう。
ソフトウェアでは命名が難しいものですが、今回は簡単です。Ruby gemの名前はwoofにします。以下は、Ruby gemのgemspecファイルwoof.gemspecです。
次に、Ruby gemのメインソースファイルlib/woof.rbを作成します。
次に、Ruby標準のextconf.rb拡張ファイルを作成します。これにより、makefileの作成に必要な情報を宣言し、ネイティブC拡張でRubyを拡張するための指示を記述できるほか、一般的なRubyコードも実行できます。
次の内容でext/woof/extconf.rbファイルを作成します。
上記の拡張コードは、Ruby用のネイティブC拡張を作成する簡単な例です。create_makefileメソッドは、ネイティブC拡張用のmakefileを作成するRubyメソッドです。create_makefileメソッドは、拡張機能の名前を引数として1つ受け取ります。拡張機能の名前は、Ruby gemの名前と同じにする必要があります。
拡張コードの最初の行では、ファイルシステムにファイルを書き込むRubyメソッドFile.writeを呼び出しています。File.writeの最初の引数は書き込み先のファイルパスで、2つ目の引数はファイルに書き込む内容です。
(無害な)悪意あるパッケージを模擬するために作成したこの例では、woof Ruby gemをインストールすると、ファイルシステムに新しいファイルが作成されます。ファイル名は/tmp/woofで、中身は空です。
Ruby gemを正常にインストールするには、有効なhello.cファイルを用意する必要があります。Rubyの標準ドキュメントにある、次のようなシンプルな*Hello World*プログラムで構いません。
最後に、Ruby gemをビルドします。このプロジェクトのルートディレクトリで、次のコマンドを実行してください。
成功すると、このプロジェクトのルートディレクトリにwoof-1.0.0.gemという新しいgemが作成されます。
このRuby gemが想定どおりに動作するかテストするには、インストールして新しいファイル/tmp/woofが作成されたことを確認します(パッケージのインストール前にファイルが存在しないことを確認してください)。次の手順で、Ruby gemをローカルフォルダーにインストールします。
mkdir ~/ruby-gem-testのように、新しい一時ディレクトリを作成します。このディレクトリ内に、
vendorという名前の新しいディレクトリを作成します。次のように、新しい
vendorディレクトリをgemディレクトリとして使うようRubyに指示し、woofgemをその中にインストールします。gem install --local /path/to/woof-1.0.0.gem --install-dir 'vendor'
任意コード実行攻撃をRuby gemのインストール方法として実行できたことを確かめるには、新しいファイル/tmp/woofが作成されていることを確認します。
Ruby gemのlockfile injectionを防ぐ方法
この記事で見てきたように、Ruby gemのインストールプロセスでは、パッケージメンテナーが依存関係のインストール時に任意のコードを実行できます。これは強力な攻撃対象領域であり、Rubyアプリケーションへのlockfile injection攻撃と組み合わせて利用されるほか、Rubyエコシステムのサプライチェーンセキュリティ全般にも影響します。
npmパッケージマネージャーには--ignore-scriptコマンドラインフラグがありますが、残念ながらRuby独自のgem CLIには、インストール時のネイティブC拡張のコンパイルを無効にする方法がありません。そのため、サードパーティのパッケージメンテナーによる任意コード実行のリスクにユーザーがさらされます。
この種の攻撃を軽減するため、次の対策をおすすめします。
Ruby gemの依存関係とその更新、およびlockfile
Gemfile.lockへの変更の管理には、自動化ボットのみを使用してください。ボットでRuby gemのバージョンを必要に応じてアップグレードし、可能な場合はセキュリティ修正を適用しましょう。開発者から
GemfileやGemfile.lockファイルへの変更の貢献を受け入れる場合は、コード変更を慎重に確認し、変更された行を一つひとつ精査してください。bundle install --deploymentを使った依存関係のインストールを推奨します。すべてのRuby gemの完全な依存関係ツリーを計算し、Gemfile.lockの内容が、Gemfileに基づいてインストールされるべきgemと一致しない場合、インストールを中断します。
この調査は、2019年に私(Liran Tal)が行った、JavaScriptエコシステムにおけるlockfile injectionのリスク開示に関する先行研究に基づいています。
Rubyのセキュリティに関するその他のリソース:
CTFを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。



