Skip to main content

npmのロックファイルが悪意あるモジュールの注入を見逃すセキュリティ上の盲点になり得る理由

著者

2019年9月24日

0 分で読めます

最近、npmエコシステムのパッケージを脅威モデリングするというアイデアを試し始めました。event-streamのインシデントは再び起こり得るのでしょうか?ほかのサプライチェーン攻撃はどうでしょう?まだ見たことのない次の攻撃ベクトルは何で、完全に防ぐことはできるのでしょうか?

そしてある日、はっと気づきました。どんなことかご紹介しましょう。プロジェクトの所有者が見逃しやすいバックドアをいかに簡単に仕込めるか。その結果、コードが安全でなくなるのです。

次の画像は、npm上のオープンソースパッケージに対して作成したプルリクエストを通じて、悪意のあるパッケージを注入した様子を示しています。

よく見て、見つけられるか試してみてください。

package.jsonでのパッケージ依存関係のバージョン更新と、折りたたまれたyarn.lockの差分を表示するGitHubのプルリクエスト

では、このPRのプルリクエストレビュー用チェックリストを確認してみましょう。

  • 追加したパッケージはエコシステムで知られており、脆弱性はない。チェック。

  • パッケージ名にタイポスクワッティングの試みはない。チェック。

  • これらは有効なバージョンで、それ自体が悪意のあるものではない。チェック。

問題なさそうです。マージしましょう!

でも待ってください。ロックファイルを注意深く確認しましたか?もちろん、していませんよね。それを責めるつもりはありません。次のような制約があるため、見逃しても無理はありません。

  • GitHubのインターフェースでは、差分が数百行を超えると、デフォルトで折りたたまれます。

  • このファイルを確認する意味があるでしょうか?機械生成されたうえ、非常に読みづらいファイルです。

こうした制約を考慮しても、実際に何が問題になるのでしょうか?

差分を展開して、中身を見てみましょう。

依存関係のバージョン更新が赤と緑で強調表示された、yarn.lockの差分を示すGitHubプルリクエスト

Yarnのyarn.lockでもnpmのpackage-lock.jsonでも、ロックファイルがある場合、インストール時には依存関係のパッケージバージョンと取得元を決める際、ロックファイルが第一の情報源として参照されます。

先ほどの話の意味が分かりましたか?ロックファイルの500行の変更は、実際にはかなり少ないほうです。npmのpackage-lock.jsonやYarnのyarn.lockファイルに、何千行もの変更が加えられるのを見たことがあります。何千行もの文字の変更を本当にレビューする人がいるでしょうか?さらに悪いことに、このロックファイルでは依存関係のバージョン変更やメタデータの更新が見られるだけで、全体として特に不自然な点はありません。

このファイルに加えられた残りの変更を確認するために少し下へスクロールすると、こんなものが見つかります。

yarn.lockの依存関係の変更を示すGitHubのプルリクエスト差分。削除された行は赤、追加された行は緑で強調表示

詳しく見ると、元のnpmパッケージmsを置き換え、GitHubリポジトリに保存した自分のバージョンを取得するように変更したことが分かります。本来は、プロジェクトのロックファイルで最初に指定されていたとおり、公式npmレジストリから取得すべきでした。

このプロジェクトではmsの複数のバージョンが使われていますが、変更したのはms@2.1.1だけで、独自にカスタマイズしたものを使うようにしました。

yarn list --pattern ms
yarn list v1.17.3
├─ @babel/helper-module-transforms@7.4.4
├─ debug@2.6.9
│  └─ ms@2.0.0
├─ express@4.17.1
│  └─ ms@2.1.1
├─ ms@2.1.2
├─ send@0.16.2
│  └─ ms@2.0.0
├─ serve-static@1.14.1
│  └─ ms@2.1.1
✨  Done in 0.49s.

これほど細かいレベルで毎回レビューするのはなかなか大変です。ロックファイルの変更を文字単位で実際にレビューする人がほとんどいないのも無理はありません。

では、私が用意したms@2.1.1は何をするのでしょうか?

このプルリクエストがマージされると、悪意のあるバージョンのms@2.1.1をコードに注入し、実行時の動作を制御できるようになります。

こうしてバックドアを仕込んだり、msモジュールのロジックを改変したり、postinstallスクリプトを実行したりできます。この実験では、https://github.com/lirantal/ms/tarball/masterというホスティングURLが明らかに怪しいのは確かです。しかし、タイポスクワッティングのドメインを購入してhttps://registry.npmgs.org/ms/でホストしたら、簡単に見抜けるでしょうか?

この実験では、msのパッケージに、テキストを出力してファイルに書き込むシンプルなpostinstallスクリプトを仕込みました。これにより、パッケージをインストールすると、yarn installの実行時にpost-installイベントが動くことを確認できます。

"scripts": {
   "postinstall": "echo im installed && echo hello > /tmp/world.txt",
}

プロジェクトベースのアプリケーションでは、ロックファイルのセキュリティが重要です。この記事で説明したような悪意ある注入攻撃の影響を受ける可能性があります。

一方、ライブラリとして使われるパッケージのロックファイルは、そのパッケージ自体の開発ワークフロー内で使用することだけを目的としています。そのため、プロジェクト(パッケージ)が別のプロジェクトの依存関係としてインストールされる際、そのロックファイルは使われません。つまり、ライブラリの場合、ロックファイルへの注入によって主にリスクにさらされるのは、プロジェクトのメンテナーやコントリビューターです。

ロックファイルのセキュリティを高めるには、何ができるでしょうか?

ロックファイルは機械が読み取ることを想定しているため、自動処理が容易です。つまり、リンターと事前設定したポリシーを適用するのに最適です。

上記の攻撃ベクトルへの対策として、この作業を支援するlockfile-lintを作成しました。Travis CIやCircle CIなどのCIサーバーで実行できる静的コード解析の短い追加ステップとして、このツールを導入しました。

少なくとも、次の条件を満たすリソースだけが配信されるよう検証しましょう。

  • HTTPS経由である

  • 信頼できるソースから取得されている

たとえば、YarnプロジェクトでCI上のチェックを実行するのは簡単です。

$ npx lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts yarnpkg.org

プロジェクトのロックファイルをリンティングした際に検出される、よくあるエラーの例を示します。

lockfile-lintがyarn.lockファイルを検証し、HTTPSではないパッケージURLを4件検出して、コード1で終了したことを示すターミナル出力。

まとめ

攻撃にはさまざまな形や種類があります。ロックファイルが狙われることさえあります。

event-streamのインシデントでは、オープンソースプロジェクトのメンテナーに対するソーシャルエンジニアリング攻撃が初めて成功する様子を目の当たりにしました。これは、npmとJavaScriptのエコシステムにおけるソフトウェアサプライチェーンセキュリティへの攻撃として、当時最も巧妙なものの一つだったと言えるでしょう。

ロックファイルへの注入も同様の手口で攻撃に利用される可能性があります。この問題を軽減する、または少なくともリスクを減らすために、適切な対策を講じましょう。次の点を検討してください。

  • ロックファイルの変更を慎重にレビューする(lockfile-lintが役立ちます)。

  • ロックファイルの変更を許可するのは、散発的なコントリビューターではなく、コアメンテナーに限定する。

  • ライブラリではロックファイルを使わない。

ロックファイルやその用途について詳しく知りたい方は、npmエコシステムのパッケージロックファイルを理解するという記事をご覧ください。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。