vm2 カスタムモジュール解決機能は、競合するホストパッケージを読み込むことで、外部パッケージの許可リストを回避する可能性があります。

### 概要

vm2 は、Node.js プロセス内で信頼できない JavaScript コードを隔離して実行するためのサンドボックスライブラリです。組み込みモジュールや外部パッケージへのアクセスを制限することができます。

`NodeVM` が、カスタムの `resolve` コールバックと合わせて `external` 許可リストを有効にすると、vm2 は要求されたパッケージ名を完全一致しない形でチェックします。例えば、許可リストが `left-pad` のみを許可する場合、攻撃者は `evil-left-pad` のような競合するパッケージ名を使用することで、このチェックを回避できます。なぜなら、このパッケージ名には許可リストに登録されている名前が含まれているからです。

競合するパッケージが、カスタムリゾルバーによって解決可能なホストパスに既に存在する場合、または、ターゲットアプリケーションのカスタムリゾルバー/依存関係管理ワークフローがパッケージをダウンロードして解決可能なパスに配置する場合、vm2 はそのパッケージをホストのコンテキストで読み込み、実行します。これにより、サンドボックス化されたコードがモジュールの許可リストを回避し、さらにホストコードの実行につながる可能性があります。

### 詳細

`NodeVM` は、サンドボックス化されたコードが読み込むことができる外部 npm パッケージを設定するために `require.external` をサポートしています。また、`require.resolve` を通じてカスタムリゾルバーもサポートしています。この組み合わせは、ビジネスプラグインシステム、ユーザー スクリプト プラットフォーム、またはサンドボックス実行環境で一般的に使用されます。アプリケーションは、信頼できる依存関係の小さなセットのみを許可し、アプリケーション自身のパッケージディレクトリを指すカスタムリゾルバーを使用します。

この脆弱性は、カスタムリゾルバーが呼び出される前の許可リストの事前チェックのロジックにあります。vm2 は、`external` 許可リストから正規表現を生成し、サンドボックス化されたコードによって提供された元のパッケージ名に対してこの正規表現を使用してチェックを行います。ただし、この正規表現は、完全なパッケージ名の境界にアンカーされていないため、元のパッケージ名に対して部分文字列のマッチングを行います。

例えば、以下の設定の場合:

```js
new NodeVM({
require: {
external: ['left-pad'],
resolve: id => require.resolve(id, { paths: [customRoot] }),
context: 'host',
builtin: []
}
})
```

意図されたポリシーは、サンドボックス化されたコードが `left-pad` のみをロードできるようにすることです。しかし、vm2 が要求されたパッケージ名に対して、`/left\-pad/` のような完全一致しないパターンを使用しているため、`evil-left-pad` や `left-pad-backdoor` のような名前も、許可リストの事前チェックを通過してしまいます。

`evil-left-pad` がチェックを通過すると、vm2 はアプリケーションによって設定されたカスタムリゾルバーを呼び出します。リゾルバーが、ホスト環境から解決可能なパスにその競合するパッケージが存在する場合、vm2 は返されたパスをロード可能なリストに追加し、`context: 'host'` モードでは、ホストの `require()` を使用してパッケージをロードします。その時点で、パッケージのトップレベルコードは、`NodeVM` の `builtin: []` のようなサンドボックスの制限を受けずに、ホストのコンテキストで実行されます。

ローカル検証では、PoC は `external: ['left-pad']` のみ許可し、`builtin: []` を設定しているため、サンドボックス化されたコードは `child_process` を直接ロードできません。しかし、サンドボックス化されたコードが `require('evil-left-pad')` を実行すると、vm2 は依然としてホストのパスから競合するパッケージをロードします。競合するパッケージのトップレベルコードは、ホストの `child_process` モジュールを正常に呼び出し、`HOST_EXEC` を出力します。

この問題は、vm2 が実行時に npm から悪意のあるパッケージを自動的にダウンロードすることを意味するものではありません。攻撃には、競合するパッケージが、カスタムリゾルバーによって解決可能なパスに既に存在しているか、またはターゲットアプリケーション自身の依存関係解決/ダウンロードのワークフローによって、そのパッケージがそのようなパスに配置されている必要があります。この前提条件により、影響を受けるシナリオは限定されますが、根本的な脆弱性は変わりません。`external` の許可リストが、完全なパッケージ名境界に対して適用されていないため、サンドボックス化されたコードが、アプリケーションによって許可されていないホストのパッケージをロードできてしまうのです。

### PoC

`poc`ディレクトリに、独立したPoC(概念実証)が添付されています。このPoCは、vm2のバージョン`3.11.5`をインストールし、一時的なホストパッケージディレクトリを作成し、許可されていない`evil-left-pad`パッケージをそこに書き込みます。

`poc`ディレクトリから、以下のコマンドを実行してください。

```bash
npm install
node poc.js
```

脆弱性が存在する場合、出力は以下のようになります。

```text
[+] vm2のバージョン: 3.11.5
[+] customRoot: /tmp/vm2-resolver-poc-xxxxxx/custom
[+] 直接的なサンドボックス内の`child_process`のrequireはブロックされました: ENOTFOUND
[+] 関連性のないパッケージはブロックされました: ENOTFOUND
[+] `require("evil-left-pad")`の結果: HOST_EXEC
[+] 結果: VULNERABLE
```

`HOST_EXEC`は、`evil-left-pad`パッケージのトップレベルのコードによって、ホスト側の`child_process.execFileSync()`を通じて生成され、許可されていないパッケージがホストのコンテキストで実行されたことを示しています。

期待される結果: `external`の許可リストに`left-pad`のみが含まれている場合、`evil-left-pad`は拒否されるはずです。

実際の結果: `evil-left-pad`は、`left-pad`という文字列が含まれているため、チェックを通過し、ホストのコンテキストで実行されます。

### 影響

影響を受ける設定では、攻撃者は、サンドボックス化されたコードを介して、`external`の許可リストに含まれていないホストパッケージを読み込み、`NodeVM`のモジュールアクセス制御を破ることができます。

攻撃者が、例えば、プラグインのアップロードディレクトリ、ユーザーが制御できる依存関係ディレクトリ、プライベートレジストリの同期ディレクトリ、アプリケーション固有の依存関係のダウンロードワークフロー、またはリゾルバーからアクセス可能なその他のパスなどを通じて、競合するパッケージの内容を制御または変更できる場合、攻撃者はさらに、ホストのNode.jsプロセスのコンテキストでコードを実行できます。攻撃者は、ホストプロセスからアクセス可能なファイル、環境変数、シークレットを読み取ったり、ホストが書き込み可能なデータを変更したり、ホストサービスを中断したりする可能性があります。

主な脆弱性の前提条件は以下の通りです。

- このアプリケーションは、信頼できない、または信頼度が低いJavaScriptコードを実行するために、vm2の`NodeVM`を使用しています。
- このアプリケーションは、すべての外部パッケージを許可するのではなく、`require.external`の許可リストを使用します。
- このアプリケーションは、カスタムの`require.resolve`を設定しています。
- 競合するパッケージは、そのカスタムリゾルバーによって解決可能なパスに既に存在するか、アプリケーションのワークフローによってそのパスにダウンロード/同期されています。
- このアプリケーションは、外部パッケージをホストのコンテキストでロードするか、関連するデフォルトの動作を維持します。

デプロイされたカスタムリゾルバーが、完全に信頼されており、攻撃者によって影響を受けることのない、固定された依存関係ディレクトリのみを検索する場合、実用的な悪用は減少します。ただし、プラグインシステム、ユーザースクリプト、アップロード可能な依存関係パッケージ、プライベートパッケージソースの同期、またはマルチテナントサンドボックスプラットフォームなどのシナリオでは、この脆弱性がサンドボックスの境界を回避する可能性があります。

### 謝辞

この脆弱性は、以下のチームによって発見されました。

- Tencent Xuanwu LabのXlabAIチーム (xlabai@tencent.com)
- Atuin Automated Vulnerability Discovery Engine
- Guannan Wang (wgnbuaa@gmail.com), Zhanpeng Liu (pkugenuine@gmail.com), Jiashuo Liang (761232680@qq.com), Guancheng Li (lgcpku@gmail.com)

影響を受けるパッケージ:
- npm (JavaScript) vm2: 影響を受けるバージョンは3.11.6以下、3.11.7で修正済み。