← ブログ
Engineering

バックエンドで信頼できないユーザーのNode.jsやPythonコードを実行する最も安全な方法は?

Yura Dubinin
Yura Dubinin

信頼できないユーザーコードは、Webバックエンドの外にある使い捨ての実行環境で動かしてください。 任意のNode.jsやPythonコードを受け付ける公開サービスでは、ジョブまたはテナントのセッションごとに適切に構成したmicroVMを用意するのが、有力な基本方針です。本番環境の認証情報を持ち込まず、不要なネットワークアクセスをブロックし、ワークロードの外側からリソース制限と有効期限を適用します。

Express、FastAPI、Djangoのアプリケーションは、ユーザー認証、ジョブのキューへの登録、結果の返却を担当します。実行の管理は別のワーカーに任せます。言語レベルの制限だけでは、この設計に必要な分離は実現できません。

適切な構成は、ユーザーが何を送信できるかによって異なります。計算機なら、小さな式言語を受け付けるだけで済みます。パッケージをインストールし、シェルコマンドを実行するコーディングエージェントには、完全な実行環境が必要です。ここでは後者を扱い、ユーザー入力からAIが生成するコードも対象にします。

まず分離境界を選ぶ

方式分離するもの任意のユーザーコードへの適性
node:vm、Pythonのevalまたはexec言語のコンテキストまたは実行スコープセキュリティ境界としては不十分
子プロセスプロセスのメモリとライフサイクルOS、ファイルシステム、ネットワーク、リソースの制御が別途必要
標準のDocker名前空間、ファイルシステムの表示範囲、リソースグループ有用な制御を備えるが、コンテナはDockerホストのカーネルを共有する
gVisorホストに届く前にアプリケーションカーネルがシステムコールを処理するワークロードとの互換性と性能が合う場合の選択肢
microVMゲストカーネルと仮想マシンの境界悪意のあるコードに対する有力な基本選択肢。ただし、ホストとゲストへのパッチ適用は引き続き必要

Dockerのセキュリティは、ホスト、デーモン、構成に依存します。gVisorはユーザー空間にアプリケーションカーネルを追加します。FirecrackerはKVMを使ってmicroVMを実行します。どれを選んでも、アプリケーションがどの認証情報やネットワーク接続先を公開すべきかは決まりません。Dockerのセキュリティ、gVisorのアーキテクチャ、Firecrackerを参照してください。

要件がさらに厳しい場合は、専用のワーカーホストやアカウントも検討してください。VMの境界はカーネル共有によるリスクを減らしますが、あらゆる脱出やハードウェアのサイドチャネル攻撃が不可能だと証明するものではありません。

Node.jsとPythonに外部サンドボックスが必要な理由

Node.js:VMコンテキストはセキュリティ境界ではない

Nodeの公式ドキュメントには、信頼できないコードに対するセキュリティ機構としてnode:vmを使ってはならないと記載されています。vm.runInNewContextに空のオブジェクトやタイムアウトを渡しても、この前提は変わりません。Node.jsのVMドキュメントを参照してください。

ワーカースレッドは並行処理に役立ちます。子プロセスは、通常のインタープリターのクラッシュからWebサーバーを守れます。ただし、どちらもファイルシステムへのアクセス、継承した環境変数、ネットワークアクセスを自動で取り除くわけではありません。サーバーと同じOS権限で動く子プロセスは、その権限が許すリソースに引き続きアクセスできます。

NodeのPermission Modelも、悪意のあるコードに対するセキュリティを保証しないと明記しています。有用な場面では追加の制限として使い、悪意のある送信コードに対しては外部の境界を設けてください。Node.jsの権限を参照してください。

Python:名前を制限してもプロセスは封じ込められない

Pythonは、execとevalが任意のコードを実行すると警告しています。__builtins__を削除したり、一部のインポートをブロックしたりしても、制約のないPythonが安全な実行サービスになるわけではありません。RestrictedPythonも言語のサブセットを提供するものと説明しており、単独でサンドボックスを提供するとは主張していません。Pythonの組み込み関数、RestrictedPythonを参照してください。

データを受け取る場合は、サイズを制限したJSON文書を受け付け、その構造を検証します。ast.literal_evalは任意のPythonコードを実行しませんが、Pythonのドキュメントには、悪意のある入力によるメモリ、CPU、スタックの枯渇リスクが記載されています。また、一般的なプログラムの実行にも対応していません。PythonのASTドキュメントを参照してください。

Pythonの全機能を使う場合は、選んだサンドボックス内で別のインタープリターを起動します。Webリクエストの解析とユーザー認証は、その外側で行います。

信頼できないコードを実行するバックエンド構成

認証付きAPI、キューと監視プロセス、独立した使い捨てサンドボックスを通じて信頼できないコードを実行する

バックエンドがジョブの記録とアクセスルールを管理します。

  1. 呼び出し元を認証し、利用枠を確認します。ソースコードをバッファーに格納する前に、リクエストのバイト数を制限します。
  2. ソースコードをデータとして保存します。ジョブをテナントに割り当て、固定の許可リストからランタイムを選びます。
  3. ジョブをキューに登録します。信頼できるワーカーが、明示的な制限と有効期限を設定した新しいサンドボックスを作成します。
  4. ジョブの入力だけを転送します。固定のインタープリターコマンドを実行し、ソースコードはファイルまたはstdinを通じて渡します。
  5. 上限を設けて出力とステータスを収集します。結果を返す前や別のサービスで使う前に、その内容を検証します。
  6. 実行環境を停止して削除します。放置されたジョブは、別のクリーンアップワーカーが処理します。

サンドボックスプロバイダーのキー、Dockerソケット、クラウドの管理用認証情報は、信頼できるワーカーに保持します。他のサンドボックスを作成または管理する認証情報を、ユーザーコードに渡してはいけません。

実行、キャンセル、ダウンロード、再開、削除の各操作で、テナントの所有権を確認します。クライアントが送るサンドボックスIDは識別子であり、アクセス権の証明にはなりません。実行の流れ全体については、AIサンドボックスガイドを参照してください。

最初のユーザージョブを動かす前に設定する制御

制御小規模なコードランナーの初期方針
CPUとRAM割り当てを固定し、ユーザーが指定するリソースフラグは拒否する
実経過時間外部から短い実行期限を設け、サンドボックスの最大有効期間も設定する
プロセスとファイルプロセス数、開けるファイル数、書き込み可能なストレージ容量を制限する
入力と出力例として、ソースコードは32 KiB、stdout/stderrの合計は64 KiB。製品に合った上限を選ぶ
ファイルシステム読み取り専用のベースイメージと小さな一時作業ディレクトリを用意し、ホストのホームやアプリケーションはマウントしない
認証情報本番環境のシークレットは持ち込まず、必要な場合だけ範囲を絞った短期間の権限を与える
ネットワーク外向きアクセスはデフォルトで拒否し、必要な接続先を明示的に許可する
テナントの状態ジョブごとに新しいワークスペースを用意するか、1つのテナントに紐づくセッションを使う
アカウントの使用量ユーザーごとに同時実行数、送信頻度、支出を制限する

CPUの割り当ては、ビジーループが動く速さを制限します。実経過時間の期限は、そのループが動き続ける時間を制限します。両方を適用してください。Dockerのドキュメントには、設定しない限りコンテナにCPUやメモリの制約はないと記載されています。Dockerのリソース制約を参照してください。

ネットワークポリシーは、公開インターネットだけでなく、プライベートサービスやクラウドのメタデータも対象にする必要があります。ジョブにHTTPが必要なら、ゲストが変更できない外向き通信プロキシやネットワーク層で許可リストを適用します。DNSの名前解決、IPv4/IPv6の接続先、リダイレクトを確認してください。ゲスト内のfetchをラッパーで包んでも、ゲストが起動できるすべてのプログラムは制御できません。OWASPのSSRF対策を参照してください。

Node.jsとPythonのテスト済みDocker起動例

以下のコマンドは、固定のサンプルプログラムを使ってコンテナ起動時の制御を示します。一般ユーザーからのコード送信を受け付ける際に必要な、実経過時間と出力を外部で監視するプロセスは含みません。 アプリケーションサーバーから分離した実行ワーカー上で動かしてください。悪意のあるコードも扱う公開サービスでは、そのワーカーを選択したVM境界内に置くか、適切なサンドボックスランタイムを使います。

信頼できる、パッチ適用済みのイメージを使ってください。以下のタグは例を示すには便利ですが、デプロイ時には確認済みのイメージダイジェストを固定し、通常のパッチ適用手順で更新します。ジョブを受け付ける前に、イメージを取得しておきます。

Node.jsの例

docker run --rm -i --init \
  --network none --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
  --user 65534:65534 --cap-drop ALL \
  --security-opt no-new-privileges \
  --memory 128m --memory-swap 128m --cpus 0.5 \
  --pids-limit 32 --ulimit nofile=64:64 \
  --log-driver none \
  node:24-bookworm-slim node - <<'JS'
console.log(JSON.stringify({answer: 6 * 7}));
JS

期待される出力は{"answer":42}です。このコマンドはUID 65534で動作し、Linuxのケーパビリティを削除し、ルートファイルシステムを読み取り専用に保ち、小さな書き込み可能な/tmpを用意します。外部ネットワークインターフェースは与えず、ホストのディレクトリもマウントしません。

Pythonの例

docker run --rm -i --init \
  --network none --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
  --user 65534:65534 --cap-drop ALL \
  --security-opt no-new-privileges \
  --memory 128m --memory-swap 128m --cpus 0.5 \
  --pids-limit 32 --ulimit nofile=64:64 \
  --log-driver none \
  python:3.13-slim-bookworm python -I -B -u - <<'PYCODE'
import json
print(json.dumps({"answer": 6 * 7}))
PYCODE

期待される出力は{"answer": 42}です。Pythonの-Iは、インポートパスやインタープリターの環境からの干渉を減らします。ただし、コンテナが許可するOSリソースをプログラムが使うことは防ぎません。Pythonのコマンドラインオプションを参照してください。

Dockerのデフォルトのseccompプロファイルを維持してください。no-new-privilegesは、setuid実行ファイルなどの仕組みによる権限昇格をブロックします。--network noneモードでは、ループバックだけが残ります。これらの制御はそれぞれ役割が異なり、互いの代わりにはなりません。Docker runのオプション、seccomp、ネットワークの分離を参照してください。

一時作業ディレクトリのnoexecオプションは、そのマウントからの直接実行を防ぎますが、インタープリターはそこにあるスクリプトを読み取って実行できます。ユーザープログラム自体を禁止するものではありません。また、128-MiBの制限はこれらの小さな例に適したものであり、あらゆるnpmパッケージやデータサイエンスのワークロードに適するわけではありません。

シェルへの文字列埋め込みを使わずにバックエンドと接続する

信頼できるワーカー上で、固定の引数からDockerまたはプロバイダーへのリクエストを組み立てます。Nodeではspawnにshell: falseを指定し、Pythonでは引数リストを渡すsubprocess.Popenを使えます。送信されたソースコードはstdinまたは固定のファイルパスを通じて渡します。sh -cに渡す文字列など、シェルコマンドの中に埋め込んではいけません。Nodeの子プロセスとPythonのsubprocessのセキュリティを参照してください。

ユーザーには、承認済みのランタイム名から選ばせます。リクエストからDockerフラグ、任意のイメージ、ホストのパス、環境変数名を指定できないようにしてください。パッケージのインストールもコードを実行します。対応する場合は、同じ分離とリソースのポリシー内で行います。

ワークロードを停止し、出力を制限し、クリーンアップを確認する

HTTPリクエストやSDK呼び出しのタイムアウトでバックエンドが待機をやめても、リモートのプログラムは動き続ける場合があります。同様に、ローカルのDocker CLIプロセスを終了しても、コンテナが動き続ける場合があります。

信頼できる監視プロセスには、次の処理が必要です。

  • 実行開始前にコンテナまたはサンドボックスのIDを記録する。
  • stdoutとstderrを少しずつ読み取り、合計バイト数に上限を設ける。
  • タイムアウトや出力超過が起きたら、ランタイムAPIで実際のワークロードを停止する。
  • 接続されたストリームを閉じるか、データを無制限に保持せずに読み捨てる。
  • 終了を確認し、使い捨ての状態を削除し、クリーンアップの失敗を記録する。

例にある--log-driver noneは、Dockerによるコンテナログの保持を防ぎます。それでも、バックエンドが読み取るストリームには上限が必要です。Dockerでは、接続中のCLIを終了する前に、対象の名前付きコンテナを停止します。microVMサービスでは、ドキュメントに記載された終了操作を使い、終了状態を確認します。

プロバイダー側で有効期限を設定できる場合は、設定してください。ワーカーの電源が落ちるとfinallyブロックは実行できないため、期限切れのジョブ記録と稼働中のリソースを照合する定期的なクリーンアップ処理を用意します。クリーンアップは繰り返しても安全に行えるようにします。クライアントが切断しただけで、取り残されたジョブのクリーンアップを成功扱いにしてはいけません。

確認した内容

2026年10月31日に、Engine 28.1.1、Linuxカーネル 6.10.14-linuxkit、ARM64、Node.js 24.22.2、Python 3.13.18を使い、Docker Desktopで両方のコマンドを実行しました。

確認項目両ランタイムの結果
固定の計算42を返した
実効ユーザーUID 65534
/etc配下への書き込みブロックした
/tmp配下での書き込みと読み取り成功した
テスト用アドレスへの外向きソケット接続ENETUNREACHで失敗した
ホストだけに設定したテスト用の環境マーカーコンテナ内には存在しなかった
リソース構成メモリ128 MiB、スワップなし、32プロセス/スレッド、CPUの半分
権限ポリシーNoNewPrivsが有効
テスト用監視プロセス下でのビジーループ2秒の期限後に停止した
テスト用監視プロセス下での過剰な出力64 KiBで出力の収集を停止し、ワークロードを終了した
前のジョブの後に作成した新しいコンテナ前の一時作業ファイルは存在しなかった

最初の出力制限の確認で、クリーンアップのバグが見つかりました。コンテナの停止後も、接続中のDocker CLIが満杯のパイプで待機していました。テスト用監視プロセスを修正し、先にワークロードを終了してから、接続中のクライアントを終了するようにしました。起動コマンドだけでは完全なランナーにならないのは、このためです。

これらの確認は、ローカルのコンテナ構成と監視プロセスの動作を対象にしています。カーネルを介した脱出のテスト、すべてのアクセス経路がブロックされていることの証明、LizardのmicroVM分離のテストは行っていません。実際にデプロイするワーカーのランタイムでも、同じ確認を繰り返してください。

結果や保存した状態も信頼しない

サンドボックスがプログラムを封じ込めても、その出力が次のシステムを攻撃することがあります。stdoutは表示する前にエスケープします。返されたJSONは検証します。送信されたPythonのpickleをバックエンドでデシリアライズしたり、返されたJavaScript文字列を評価したりしてはいけません。Pythonのpickleに関する警告、OWASPの出力処理を参照してください。

生成したHTMLやプレビューは、ダッシュボードと認証情報を共有しないオリジンから配信します。ダウンロードするファイルは名前、サイズ、種類を制御し、ゲストが作成したシンボリックリンクをたどってホストのパスに入らないようにします。アーカイブの展開や解凍の処理量も制限します。

毎回新しい状態を用意することで、あるテナントが別のテナントのファイル、パッケージキャッシュ、メモリ内の値を読むのを防ぎます。永続セッションには、明確な所有権と保持のルールが必要です。一時停止したサンドボックスは状態を保持するため、次のユーザーのクリーンな環境として使ってはいけません。スナップショットや共有ボリュームにも同じルールを適用します。

Lizardを活用できる場面

Lizard Sandboxesは、それぞれ独自のカーネルを持つFirecracker microVMでコードを実行します。Lizard SDKはJavaScript/TypeScriptとPythonに対応し、ファイル操作、コマンド実行、ステータス確認、ライフサイクル制御を提供します。バックエンドは、独自のワーカーホストを運用する代わりに、このAPIを通じて実行環境を管理できます。Lizard Sandboxesのドキュメントを参照してください。

管理用APIキーはバックエンドに保持します。悪意のあるコードを扱うジョブでは、ゲスト内のプロジェクト認証情報を有効にするlizardTokenを渡したり、タスクに不要なエージェントの認証情報を付与したりしないでください。有効期間を明示的に設定し、上限を設けて結果を収集した後に、使い捨てのジョブを終了します。Lizard SDKリファレンスを参照してください。

Firecrackerによる分離は、外向き通信のブロックを意味しません。 デプロイ済みのネットワークポリシーが、必要な接続先の制限を適用しているか確認してください。現在のSDKの作成オプションのリファレンスには、外向き通信の許可リストは記載されていません。存在しないフラグを作ったり、ゲスト側のプロキシ設定で許可リストを強制できると思い込んだりしてはいけません。厳格な外向き通信の制御が必須なら、そのワークロードを受け付ける前に、制限を強制できる構成を確認してください。

まず、ジョブに許可するファイル、ネットワークアクセス、リソース予算を定義します。その制限を設定した新しい環境でテストします。実行APIについてはLizardクイックスタートを、コストの見積もりにはサンドボックスの料金比較を参照してください。

AI で構築。Lizard で公開。

本番公開にプラットフォームチームは必要ありません。クラウド全体が、1 つの CLI コマンドですぐ使えます。

無料で始める
サービス
—
アドオン
—
デプロイ
—
Sandbox の起動
—

当サイトでは、基本機能と分析のために Cookie を使用しています。詳しくはCookie ポリシーをご覧ください。