信頼できないJavaScriptコードをサーバーで実行したい——プラグインシステム、コードエディタのプレビュー機能、ユーザー定義スクリプトの実行環境など、そのニーズは思いのほか多い。しかし、適切なサンドボックスを選ばなければセキュリティホールに直結する。今回、Simon WillisonがClaude Codeを使ってこのテーマを深掘りした調査結果が公開された。
なぜJavaScriptサンドボックスは難しいのか
Node.jsはデフォルトでシステムリソースへのフルアクセスを前提に設計されている。require('fs')やchild_processを呼ばれたら終わり——そのリスクを封じるのが「サンドボックス」だ。しかし、JavaScriptのプロトタイプチェーンやグローバルスコープの特性から、完全な隔離は技術的に非常に難しい。vm2の廃止(メンテナがセキュリティ上の理由でプロジェクトを放棄)はその象徴的な出来事だった。
6つのアプローチ徹底比較
調査では以下のオプションが評価された:
-
Node.js組み込み
worker_threads:別スレッドで実行するが、デフォルトでは隔離不十分node:vm:コンテキスト分離はできるが、プロトタイプ汚染でエスケープ可能- Permission Model(Node.js 20+):
--allow-fs等でファイルアクセスを制限できる新機能
-
npmパッケージ
- isolated-vm:V8の
Isolateを直接使い、メモリ・CPUを厳格に制御。現状最も信頼性が高い - vm2:過去の定番だがセキュリティ上の理由でメンテナンス終了。新規採用は非推奨
- isolated-vm:V8の
-
代替エンジン
- quickjs-emscripten / QuickJS-NG:QuickJSをWasm化してNode上で動かす。V8より遅いが完全な隔離を実現
- ShadowRealm:TC39提案。ブラウザ・Node両対応を目指すが、現時点では制限多め
- Deno Workers:Deno自体を使う場合の選択肢。パーミッションモデルが洗練されている
実装の第一歩:isolated-vmのコード例
import ivm from 'isolated-vm';
const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 128MB制限
const context = await isolate.createContext();
// グローバルにlogを注入
await context.global.set('log', new ivm.Reference((...args) => console.log(...args)));
const script = await isolate.compileScript('log.applyIgnored(undefined, ["hello from sandbox"])');
await script.run(context);
CPU時間制限やメモリ制限をコンストラクタで指定できる点がisolated-vmの強みだ。
まとめ・選び方ガイド
用途別のおすすめはこうなる:
- 本番・高セキュリティ要件 →
isolated-vm一択(V8 Isolate + メモリ制限) - 完全隔離・パフォーマンス不問 →
quickjs-emscripten(Wasm隔離) - Node.js 20+の制御で十分 → Permission Model +
worker_threadsの組み合わせ - vm2は今すぐ移行 → 代替は
isolated-vmかquickjs-emscripten
Simon WillisonがClaude Codeに調査を依頼し、当初の質問(worker_threadsのサンドボックス活用)を大幅に超えた比較レポートが生成されたというプロセスも興味深い。AIをリサーチツールとして使う実例としても参考になる。元記事のリンク先には調査レポート全文が掲載されているので、実装を検討している方はぜひ読んでほしい。