モデル出力をNode.jsのシェルで実行する問題

モデル出力をNode.jsのシェルで実行する問題

説明

LLMの出力は、モデルの調整やプロンプトの指示にかかわらず、後続のシステムに対する信頼できない入力として扱う必要があります。プロンプトインジェクションだけでなく、通常の依頼、取得したコンテンツ、モデルの誤りも、予期しないコマンドを生成する場合があります。その出力をシステムシェルへ渡すと、間接的にモデルへコマンド実行権限を与えることになります。

実際のリスクは入力元、プロセス権限、実行環境、別途行う認可や確認によって異なります。出力をデータとして検証することと、そのデータが示す操作を許可することは別です。

想定される影響

  • 生成された文字列を exec や execSync に渡すと、シェルがメタ文字や追加の引数を解釈し、コマンドインジェクションにつながるおそれがあります。
  • アプリケーションの権限で任意のコマンドや別のプログラム・スクリプトを実行される可能性があります。
  • ファイル、認証情報、データベース、デプロイ権限へアクセスできると、情報漏えい、データ変更、サービス停止へ影響が広がるおそれがあります。

対処方法

  1. モデルが返した自由形式のコマンドを実行しないでください。プロセス内のライブラリや、アプリケーションが定めた固定の処理を優先してください。
  2. 構造化出力も信頼しないでください。JSONスキーマが確認するのは形式です。許可する処理、引数、対象、ユーザーの権限は、決定的なアプリケーションコードで別途検証してください。
  3. OSの機能にはライブラリAPIを優先してください。外部プロセスが必要な場合は、実行ファイルをコードに固定し、引数を個別に検証してから execFile や spawn に配列で渡し、シェルを無効にしてください。コマンド全体のエスケープや、sanitize という名前の関数だけに依存しないでください。
  4. 強い権限を使う操作、取り消せない操作、外部へ影響する操作には、具体的な内容を示す確認手順を設けてください。モデルと連携するプロセスの権限も最小限にしてください。
  5. モデル、プロンプト、ツール、出力スキーマの変更後は、直接・間接のプロンプトインジェクションと不正な形式の出力を再テストしてください。

例

変更前

javascript
import OpenAI from "openai";
import { exec } from "node:child_process";

const client = new OpenAI();

async function runGeneratedCommand(userRequest) {
  const response = await client.responses.create({
    model: "gpt-5.5",
    input: `Write a shell command for this request: ${userRequest}`
  });

  exec(response.output_text);
}

変更後

javascript
import OpenAI from "openai";

const client = new OpenAI();
const ALLOWED_TARGETS = new Set(["api-staging", "worker-staging"]);
const OPERATIONS = Object.freeze({
  status: target => getDeploymentStatus(target)
});

function parseAction(text) {
  const value = JSON.parse(text);
  if (
    !value ||
    typeof value !== "object" ||
    typeof value.operation !== "string" ||
    typeof value.target !== "string" ||
    !Object.hasOwn(OPERATIONS, value.operation) ||
    !ALLOWED_TARGETS.has(value.target)
  ) {
    throw new Error("Unsupported model action");
  }
  return value;
}

async function runAllowedAction(userRequest) {
  const response = await client.responses.create({
    model: "gpt-5.5",
    input: `Choose a read-only operation for: ${userRequest}`
  });

  const action = parseAction(response.output_text);
  return OPERATIONS[action.operation](action.target);
}

解説:

  • 変更前: 生成された文字列を exec(...) に直接渡すため、シェルがメタ文字や追加のコマンドを解釈します。
  • 変更後: 事前に定めた読み取り専用の処理と対象だけを選べるようにし、モデル出力自体は実行しません。getDeploymentStatus とエラー処理は省略しています。現在のユーザーが選択した対象へアクセスできるかも、別途確認してください。

他の出力先での扱い

HTML・Markdown、SQL、ファイルパス、端末などへ渡す出力も、その文脈に合う処理が必要です。HTMLではテキスト出力と文脈別のエンコードを優先し、マークアップが必要な場合にDOMPurifyなどのHTMLサニタイザーを使ってください。ネイティブの Element.setHTML() は実行環境の対応を確認してから使用し、setHTMLUnsafe() を一般的な対策としないでください。

参考資料