FlawOpen

flawopen.com/コマンドインジェクション/Node child_process.exec

Node.js child_process.exec()のコマンドインジェクションリスク

参考页面
やさしい説明

コマンド文字列をexec()に渡すのは、文を組版担当者に渡し、句読点で指示の区切りを判断してもらうようなものです。信頼できない文字列にshell構文が含まれると、shellが制御命令として解釈することがあります。execFile()は固定したプログラムと個別の引数を渡すため、デフォルトではshell解析が行われません。

脆弱
const { exec } = require('node:child_process');

function inspectFile(filename) {
  return exec(`file ${filename}`);
}
修正済み
const { execFile } = require('node:child_process');

const allowed = new Map([
  ['report-1', '/srv/reports/1.pdf'],
  ['report-2', '/srv/reports/2.pdf']
]);

function inspectReport(reportId) {
  const path = allowed.get(reportId);
  if (!path) throw new Error('Unknown report');
  return execFile('/usr/bin/file', ['--', path], {
    shell: false,
    timeout: 3000,
    maxBuffer: 64 * 1024
  });
}

execFile()がコマンドインジェクションのリスクを下げる理由

exec()はshellを起動し、コマンド文字列を渡します。攻撃者が制御できる文字列をコードが埋め込むと、shellはメタ文字を構文として解析します。これがCWE-78で説明されるコマンドインジェクションの経路です。execFile()は引数配列で指定した実行ファイルを直接起動し、デフォルトではshellを生成しません。そのため、shell区切り文字がshellによって再解析されることはありません。例では、呼び出し側のIDを固定パスに対応付けています。ただし、対象プログラムの安全性まで保証するものではありません。受け付ける引数を検証し、対応している場合は--でオプション解析を終了し、信頼できない値にshell: trueを指定しないでください。

危険なexec()呼び出しを見つける

exec()とexecSync()を検索し、各コマンド文字列をリクエストパラメーター、DBフィールド、ファイル名、環境変数まで追跡します。実行ファイルと引数の境界を確認します。可能なら直接APIを使い、それ以外は固定実行ファイル、検証済み引数、実行時間と出力量の制限を組み合わせたexecFile()を使います。

grep -RInE 'exec(Sync)?[[:space:]]*\(' --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' .

同期版

execSync()の場合は?

execSync()も同じshellベースの文字列を使うため、同じインジェクションリスクがあり、さらにイベントループをブロックします。execFileSync()はデフォルトでshell解析を避けますが、実行ファイルと引数の検証は必要です。

参考資料

関連項目 OS コマンドインジェクション(CWE-78)の仕組みと各言語での防ぎ方 — flawopen.com Node.js exec() と execFile():違いと安全な使い方 Java Runtime.exec()とCommand Injection:何が違う?