依存関係の混乱をマッピングする:ソースマップファイルを用いた新たな検出アプローチ
依存関係の混乱(Dependency Confusion)の脆弱性を掘り下げ、新たな検出・悪用手法を紹介したうえで、この脆弱性に伴うリスクを緩和するための具体的な手順を示します。
はじめに
ソフトウェアサプライチェーンは、開発からデプロイに至るまでのコードのライフサイクルに関わるあらゆる要素を含みます。これには、コード、バイナリ、そしてリポジトリやパッケージマネージャーから取得される依存関係が含まれます。
サードパーティのコードを使用すると時間と労力を節約できますが、潜在的なセキュリティリスクも生じます。これらの依存関係に欠陥があると、サプライチェーン全体のセキュリティが損なわれる可能性があるからです。
企業の中には、サードパーティのコードの代わりに、プライベートなレジストリや、プライベートスコープを持つパブリックなレジストリでホストされる内部の依存関係を使用することを選ぶところもあります。しかし、このアプローチもセキュリティ上の問題と無縁ではなく、ソフトウェアサプライチェーンにおける新たな脆弱性パターンである依存関係の混乱(Dependency Confusion)につながる可能性があります。
依存関係の混乱は、さまざまなパッケージマネージャーで発生し得ます。この記事では、Javascript向けに広く使われているパッケージマネージャーであるnpmに焦点を当てます。
依存関係の混乱の概要
依存関係の混乱は、悪意のある攻撃者がパッケージマネージャー(NPM、PyPI、RubyGems、JFrog Artifactory...)をだまし、正規のパッケージや依存関係の代わりに悪意のあるものをダウンロードさせることに成功したときに発生します。これは、いくつかの設定ミスや誤った前提によって引き起こされます。主なものは次のとおりです。
タイプミス
依存関係の混乱が起こる最も単純な経路の一つが、タイプミスです。攻撃者は、開発者がタイプミスや見落としによって、意図したパッケージではなく悪意のあるパッケージをうっかりインストールしてしまうことを見越します。典型的には、人気のあるパッケージに似ているものの、名前にタイプミスやわずかな違いがある悪意のあるパッケージを公開します。
パッケージの優先順位
開発者は、パッケージマネージャーが常にパブリックなパッケージよりも内部のパッケージを優先すると思い込み、内部には存在するもののパブリックなレジストリには存在しないパッケージ名に依存してしまうことがあります。これは、悪意のある攻撃者がそのパッケージ名を突き止め、パブリックなレジストリでその名前を取得しようと決めるまではうまく機能します。
以下は、javascriptプロジェクトのpackage.jsonファイルの例です。
{
"name": "my-project",
"version": "1.0.0",
"description": "Project 1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Corp",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"lodash": "^4.17.21",
"axios": "^0.25.0",
"moment": "^2.29.1",
"internal-corp-package": "^1.5.0"
},
"devDependencies": {
"nodemon": "^2.0.15",
"eslint": "^8.6.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.1"
}
}
一つの依存関係が目を引きます。internal-corp-packageです。その名前から、内部のレジストリに由来するもの、または内部での使用を意図したものであることがうかがえます。これがパブリックなレジストリに存在するかどうかを確認できます。

これは、このパッケージが内部のレジストリにしか存在しないことを意味します。悪意のある攻撃者がこの内部パッケージを発見し、同じ名前の悪意のあるパッケージをnpmのパブリックレジストリに公開することにしたと仮定すると、次のようなワークフローになります。

上の図を詳しく見ていきます。
- レジストリが明示的に設定されていない場合
これは、ユーザーがnpm config set registry http://[internal registry]を実行するか、.npmrcファイルで指定するかのいずれかの方法で、内部のレジストリを使うようnpmに明示的に指示していないことを意味します。この場合、npmはインストールの際に自動的にnpmのパブリックレジストリhttps://registry.npmjs.org/をデフォルトとして使用します。つまり、攻撃者が公開した悪意のあるパッケージをインストールすることになります。
- レジストリが適切に設定されている場合
ここでは、次の2つのシナリオがあります。
- 指定されたパッケージのバージョンが厳密な場合
指定されたパッケージのバージョンが1.0.0のように厳密で、そのバージョンがプライベートレジストリに存在する場合、npmは自動的にそれをデフォルトとして使用します。つまり、正規のパッケージをインストールします。バージョンが厳密に指定されたpackage.jsonの例は次のとおりです。
{
...
"dependencies": {
...
"internal-corp-package": "1.0.0"
},
...
}
- 指定されたパッケージのバージョンが緩い(範囲指定の)場合
厳密なバージョン以外にも、npmではバージョンを3つの異なる方法で表現できます。
^1.0.0:npmはバージョン1.0.0、または1.0.0より大きく2.0.0未満の任意のバージョン(1.9.5など)をインストールします(サブバージョンとパッチレベルはアップグレードできますが、メジャーバージョンはアップグレードできません)。~1.0.0:npmはバージョン1.0.0、または1.0.0以上1.1.0未満の任意のバージョン(1.0.9など)をインストールします(アップグレードできるのはパッチレベルのみです)。*:インストールされるバージョンに制限がないことを意味します。この場合、npmは最新バージョンをデフォルトとして使用します。
上記のいずれの場合も、攻撃者は適切なバージョンを用意できれば、依然として依存関係の混乱攻撃を実行できます。たとえば、指定されたバージョンが^1.0.0であれば、攻撃者はバージョン1.0.1の悪意のあるパッケージを作成して公開でき、それはチェックを通過してしまいます。
依存関係の混乱がもたらす影響
依存関係の混乱は、かなり容易にリモートコード実行(RCE)へと発展させることができます。攻撃者には、悪意のあるパッケージを通じて任意のコードを実行する方法が数多くあります。最も簡単な方法の一つがpackage.jsonのscriptsプロパティを使うもので、攻撃者はインストールのどの段階でも実行するコマンドを指定できます。たとえば次のとおりです。
{
"name": "internal-corp-package",
"version": "1.0.1",
"description": "Definitely not a malicious package!",
"main": "index.js",
"scripts": {
"postinstall": "ping [malicious host]",
},
"author": "",
"license": "ISC"
}
取得されていない組織名
NPMレジストリには組織スコープを使用する機能があり、その場合パッケージ名は@org/packageのようになります。組織によっては、自社の組織名をプライベートレジストリには登録していても、npmのパブリックレジストリには登録していないことがあります。悪意のある攻撃者がnpmのパブリックレジストリでその組織名を取得できれば、それを使って依存関係の混乱攻撃を実行できます。
ソースマップファイルを通じた依存関係の混乱の悪用
ソースマップファイルは、単にソースマップと呼ばれることも多く、プログラムのソースコードと、ブラウザーやその他のJavaScriptエンジンで実行される変換後のコードとの対応関係を提供するファイルです。Web開発では、特にミニファイやトランスパイルされたコードを扱う際に、デバッグやエラー報告を支援するために広く使われています。
ソースマップは開発やデバッグの目的には便利ですが、セキュリティ上の影響をもたらすことがあります。特に、本番ビルドで意図せず露出している場合です。ソースマップファイルが本番ビルドで公開されたまま放置されていると、攻撃者は元のミニファイされていないソースコードを再構築し、その依存関係(通常はnode_modulesフォルダーにあります)を列挙できる可能性があります。依存関係の一覧にアクセスできることは、依存関係の混乱にとって極めて重要な意味を持ち得ます。
webpackによって生成される典型的なソースマップファイルは次のようになります。
{
"version": 3,
"file": "bundle.js.map",
"sources": [
"webpack:///./src/file1.ts",
"webpack:///./src/file2.ts",
"webpack:///./node_modules/internal-corp-package/utils.js"
],
"sourcesContent": [
"console.log('This is file1');",
"console.log('This is file2');",
"console.log('This is internal package utils file');"
],
"mappings": "AAAA,OAAO,EAAE;AACP,OAAO,CAAC,GAAG,EAAE",
"sourceRoot": ""
}
このソースマップから、src/file.ts、src/file2.ts、node_modules/internal-corp-package/utils.jsの元のミニファイされていないコードを復元できます。
ソースコードに依存関係が直接埋め込まれているのを見つけられたケースもありました。そうした依存関係の名前にアクセスできることも、同様に依存関係の混乱にとって極めて重要な意味を持ち得ます。
この手法がさらに懸念されるのは、攻撃者が上記の依存関係の元のコードにアクセスできるようになるため、コードを壊すことなく密かにバックドアを注入できる点です。これにより、攻撃者は標的の環境を稼働させたまま悪意のあるコードを実行できます。前述のscriptsフィールドは容易に見つけられるのとは異なり、この脆弱なパッケージに依存するアプリケーションは侵害の兆候を見せずに動作し続けるため、検出はさらに困難になります。
依存関係の混乱のリスクに加えて、攻撃者が内部のjavascriptアプリケーションの元のソースコードを復元すると、そのコードには、ミニファイされたコードでは検出が難しいハードコードされた認証情報や、トランスパイルされたコードでは削除されるはずのコメントが含まれている可能性があります。
バグバウンティプログラムのスキャン
当社の分析では、さまざまなバグバウンティプログラムの191のアセットに対してスキャンを実行し、そのうち約5%がこの手法を用いて依存関係の混乱に対して脆弱であることが判明しました。

依存関係の混乱攻撃からの防御
依存関係の混乱に対する特効薬はありませんが、リスクをある程度緩和するために取れる手順があります。主なものは次のとおりです。
- 組織スコープを使用する
NPMレジストリには、組織名を登録してスコープとして使用するオプションがあります。組織は内部のレジストリで使用できますが、npmのパブリックレジストリにも登録しておく必要があります。こうすることで、攻撃者は自社の組織へのアクセス権がない限り、依存関係の混乱を実行する手段がなくなります。
- 内部パッケージの名前をnpmのパブリックレジストリに登録する
攻撃者に先手を打つ一つの方法は、内部で使用しているパッケージ名をnpmのパブリックレジストリで取得しておくことです。ただし、一部のパッケージを見落とす可能性があるため、このアプローチは万全ではありません。
- パッケージの完全性を検証する
npmパッケージの各バージョンにはsha512ハッシュがあります。このハッシュを使って、ダウンロードする前にパッケージの完全性を検証できます。
- バージョンの固定
^1.0.0のような緩いバージョンを使う代わりに、正確な1.0.0バージョンを使用できます。こうすれば、NPMがより高いバージョンをデフォルトとして使用することはありません。ただし注意点として、バージョンの固定は推移的な依存関係には適用されません。
- パッケージのロック
package-lock.jsonやyarn.lockファイルを活用して、プロジェクトで使用する依存関係の特定のバージョンをロックします。これにより、インストール時の依存関係のバージョンの予期せぬ変更を防ぎ、直接の依存関係と推移的な依存関係の両方をロックできます。
まとめ
結論として、依存関係の混乱はソフトウェアサプライチェーン全体のセキュリティを損ない得る差し迫った懸念へと発展しており、それに対する防御策を講じることは極めて重要になっています。
タグ:
supply chain