vm2: NodeVM node: プレフィックス、否定、組み込み関数、拒否、バイパス、child_process

## 概要

NodeVMは、`require()`の解決時に`node:`プレフィックスが付いた組み込み関数指定子を正規化しますが、ユーザーが提供するワイルドカードポリシーにおける否定的な組み込み関数エントリは正規化されません。

その結果、以下の設定では:

```js
new NodeVM({
require: {
builtin: ['*', '-node:child_process']
}
});
```

標準的な`child_process`組み込み関数が拒否されません。サンドボックス化されたコードは、`child_process`と`node:child_process`の両方を`require()`で読み込むことができ、`execSync`や`spawn`などのプロセス生成APIを持つホストモジュールを受け取ります。

以下の安全性の検証は、モジュールと関数の到達可能性のみを確認し、OSコマンドの実行は行いません。

## 影響を受けるモード

NodeVM。

## 影響を受ける設定

```js
new NodeVM({
require: {
builtin: ['*', '-node:child_process']
}
});
```

これは、`node:`プレフィックスを使用して組み込み関数を拒否するユーザーに影響を与えます。`-node:child_process`が`require('node:child_process')`と`require('child_process')`の両方を拒否すると期待している場合、実際には期待通りに動作しません。

## 影響を受けるファイル/関数

- `lib/builtin.js`
- `makeBuiltinsFromLegacyOptions`
- ワイルドカード組み込み関数の展開
- 完全一致による否定エントリのチェック: `builtins.indexOf(\`-${name}\`)`
- `addDefaultBuiltin`
- `lib/resolver.js`
- `Resolver.resolve`
- `lib/setup-node-sandbox.js`
- `requireImpl`
- 組み込み関数読み込み前の`node:`プレフィックスの削除

## 根本原因

`lib/setup-node-sandbox.js`では、組み込み関数のファイル名を解決した後、組み込み関数を読み込む前に`node:`プレフィックスが削除されます。

```js
if (localStringPrototypeStartsWith(filename, 'node:')) {
id = localStringPrototypeSlice(filename, 5);
let nmod = cacheBuiltins[id];
if (!nmod) {
nmod = loadBuiltinModule(id);
if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND');
cacheBuiltins[id] = nmod;
}
return nmod;
}
```

しかし、`lib/builtin.js`では、ワイルドカードによる否定エントリが、`BUILTIN_MODULES`内の名前との完全一致によってチェックされます。

```js
if (builtins.indexOf(`-${name}`) === -1) {
addDefaultBuiltin(res, name, hostRequire);
}
```

`BUILTIN_MODULES`には、`node:child_process`ではなく、標準名である`child_process`が含まれています。したがって、`-node:child_process`は`child_process`を除外せず、`addDefaultBuiltin()`はホストの組み込みモジュールを登録します。

## セキュリティ境界の越境

サンドボックス内のコードが、埋め込み元がアクセスを制限しようとしたホストの組み込みモジュールに到達しました。

越境された境界:

- サンドボックス -> ホストの`child_process`組み込みモジュール
- サンドボックス -> ホストのプロセス生成関数への参照

## 影響

確認された影響:

- サンドボックス内で`require('child_process')`が成功します。
- サンドボックス内で`require('node:child_process')`が成功します。
- 返されたモジュールは、`execSync`と`spawn`を関数として公開します。

最も深刻な確認された影響は、ホストのプロセス生成APIへのアクセスです。この検証では、コマンドは実行されません。

この検証ではコマンドは実行されませんが、ホストの`child_process`モジュールとそのプロセス生成APIへのアクセスが確認されます。信頼できないサンドボックス内のコードにとっては、これはコマンド実行能力と同等です。

## 安全なローカルでの再現

Node.js `v24.14.0`でテスト済み。

この検証は、モジュールと危険な関数がアクセス可能かどうかのみを確認します。プロセスを生成したり、OSコマンドを実行したりすることはありません。

```js
'use strict';

const { NodeVM } = require('./');

function probe(builtin) {
const vm = new NodeVM({
require: {
builtin
}
});

return vm.run(`
const out = {};

for (const spec of ['child_process', 'node:child_process']) {
try {
const cp = require(spec);
out[spec] = {
loaded: true,
execSyncType: typeof cp.execSync,
spawnType: typeof cp.spawn,
moduleToStringTag: Object.prototype.toString.call(cp)
};
} catch (e) {
out[spec] = {
loaded: false,
name: e && e.name,
code: e && e.code,
message: e && e.message
};
}
}

module.exports = out;
`);
}
```

```javascript
console.log(JSON.stringify({
nodeVersion: process.version,
denyNodePrefixed: probe(['*', '-node:child_process']),
denyCanonical: probe(['*', '-child_process'])
}, null, 2));
```

観察された結果:

```json
{
"nodeVersion": "v24.14.0",
"denyNodePrefixed": {
"child_process": {
"loaded": true,
"execSyncType": "function",
"spawnType": "function",
"moduleToStringTag": "[object Object]"
},
"node:child_process": {
"loaded": true,
"execSyncType": "function",
"spawnType": "function",
"moduleToStringTag": "[object Object]"
}
},
"denyCanonical": {
"child_process": {
"loaded": false,
"name": "VMError",
"code": "ENOTFOUND",
"message": "Cannot find module 'child_process'"
},
"node:child_process": {
"loaded": false,
"name": "VMError",
"code": "ENOTFOUND",
"message": "Cannot find module 'node:child_process'"
}
}
}
```

## 期待されるセキュリティ上の動作

`-node:child_process` と `-child_process` は同等であるべきです。

どちらかの表記が拒否された場合、以下の両方が失敗するべきです。

```js
require('child_process')
require('node:child_process')
```

## 推奨される修正方法

1. 許可/拒否の比較を行う前に、組み込み関数の名前を正規化する。
- ユーザーが提供した組み込み関数のエントリから `node:` を削除する。
- 正規化する前に、エントリが否定形 (`-...`) であるかどうかを保持する。
- 1つの正規化された組み込み関数のキーを保存して比較する。

2. 同じ正規化を以下にも適用する。
- ワイルドカードを使用した否定エントリ
- 明示的な許可リストエントリ
- オブジェクト形式の組み込み関数エントリ
- モック/オーバーライドキー(`node:` の表記をサポートすることを意図している場合)

3. リグレッションテストの追加:
- `builtin: ['*', '-node:child_process']` は `child_process` をブロックします。
- `builtin: ['*', '-node:child_process']` は `node:child_process` をブロックします。
- `builtin: ['*', '-node:fs']` は `fs` と `node:fs` をブロックします。
- `builtin: ['*', '-node:fs/promises']` と `-fs/promises` は、一貫した動作をします。
- 危険な組み込み機能は、`node:` という表記で明示的に要求された場合でも、引き続きブロックされます。

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