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