Tech-Solve-MyDatabase

OSコマンドインジェクションを防ぐためのシェル実行の回避と引数検証の鉄則

当ブログはWeb広告を導入しています(景表法による表示)
◎ 10秒解説
  • 外部入力をシェルに渡すことでサーバーを乗っ取りられる、単純ながら極めて強力な脆弱性
  • `exec`ではなく引数を配列で渡す`spawn`を利用し、シェルの特殊文字解釈を物理的に回避
  • 入力値のホワイトリスト検証と最小権限の徹底により、万が一の注入時も被害を最小化

OSコマンドインジェクションとは?

OSコマンドインジェクションは、攻撃者がサーバー上で任意のOSコマンドを実行させる脆弱性です。

アプリケーションが外部入力をシェルに渡して実行する際に、適切にエスケープやバリデーションを行わないことで発生します。この攻撃が成功すると、サーバー内のファイル奪取、バックドアの設置、他のサーバーへの踏み台利用など、致命的な被害につながります。

脆弱性が発生する仕組み

「ユーザー入力されたIPアドレスにpingを打つ」といった機能が典型的な例です。

// 脆弱な実装例
const target = req.query.target;
exec(`ping -c 3 ${target}`); // 入力をそのままシェルに渡す

攻撃者が target=127.0.0.1; rm -rf / と入力すると、 ping 実行後に続けて rm -rf / が実行されてしまいます。シェルにおける「セミコロン(;)」や「パイプ(|)」などの特殊文字が、攻撃の手がかりとなります。

根本的な対策:シェルを介さない実行

最も推奨される対策は、**「OSコマンドを直接実行せず、言語標準のライブラリやAPIを使用すること」**です。

対策のポイント(仕様まとめ)

対策手法 実装の方向性 エンジニアとしての所感
シェル呼び出しの回避 exec ではなく spawnexecFile で引数を配列として渡す そもそも「シェル」を起動させないことで、メタ文字の解釈を物理的に防ぎます。
ホワイトリスト検証 入力値を許容されるパターン(数値や英数字のみ)に厳格に限定する IPアドレスなら正規表現で形式をチェックし、それ以外は即エラーにします。
最小権限で実行 Webサーバープロセスに不必要な特権を与えない 万が一コマンドが注入されても、 root 権限がないことで被害を限定できます。

2026年3月の現場感: シェル注入は「便利機能」から戻ってくる

2026年3月のコミュニティを追うと、OSコマンドインジェクションは昔ながらの CGI 的コードだけでなく、管理画面の診断機能、動画変換、Zip 展開、Git 操作ラッパー、AI ワークフローの補助スクリプトから再発している印象が強いです。つまり「バックエンドが少しだけ OS に触る便利機能」が危険源です。

特に最近は、AI 補助で「まず shell command で動かす」実装が生成されやすく、そこへユーザー入力や設定値が混ざって事故になるパターンが増えています。2026年時点で重要なのは、危険文字を全部弾くことではなく、文字列としてコマンドを組み立てない文化をチームで固定することです。

私ならレビュー時に次の点を優先します。

  1. execsystemsubprocess(..., shell=True) が残っていないか
  2. 設定ファイルの値や管理画面入力が、そのままコマンド引数へ入っていないか
  3. 本当に外部コマンドが必要か、標準ライブラリで代替できないか

トラブルシューティング:セキュアな外部コマンド実行手順

どうしても外部コマンドを呼び出す必要がある場合は、以下の手順を守ります。

1. 引数を分離した呼び出し

// NG: 文字列として結合してシェルで実行
exec(`ping -c 3 ${target}`);

// OK: 配列として引数を渡し、シェルを介さず実行
const { spawn } = require('child_process');
const ping = spawn('ping', ['-c', '3', target]);

2. 環境変数のクリア

外部コマンド実行時に、予期せぬ環境変数の影響を受けないように、環境変数を明示的にクリアまたは制限して渡します。

3. パスの絶対指定

実行ファイルを名前だけで指定せず(例: ls )、 /bin/ls のようにフルパスで指定することで、パス改ざん(PATHインジェクション)のリスクを低減します。

4. 設定値も「信頼しない入力」として扱う

本番事故では、フォーム入力よりも .env、管理画面設定、ジョブキューの payload など「内部値」のほうが盲点になりがちです。ユーザー入力と同じく、設定値も allowlist や型検証を通してから引数へ渡すべきです。

5. タイムアウトと作業ディレクトリを固定する

安全に実行できても、長時間ぶら下がる外部コマンドは障害の種です。timeout、作業ディレクトリ、環境変数、標準入力の有無を明示し、実行条件を固定したラッパー関数に寄せると保守しやすくなります。

実務で事故を減らす確認表

確認項目 合格ライン ありがちな事故
実行API spawn / execFile / 標準ライブラリ利用 exec で一発実装
引数生成 配列で固定、allowlistあり テンプレート文字列で結合
実行ファイル 絶対パス指定 PATH 汚染で別バイナリ実行
権限 専用ユーザー、最小権限 root でそのまま稼働
運用 timeout、監査ログ、失敗時の扱い明確 ハングしても気づけない

よくある質問(FAQ)

Q: 全ての特殊文字をエスケープすれば安全ですか?

A: 理論上は可能ですが、OSやシェル(bash, zsh, cmd.exe)によって特殊文字の挙動が異なるため、漏れなく対応するのは極めて困難です。シェル回避が第一選択です。

Q: PHPの escapeshellarg は信頼できますか?

A: 一定の防御効果はありますが、歴史的には脆弱性が見つかったケースもあります。可能な限り exec 系の関数自体の使用を避ける設計にすべきです。

Q: 社内専用ツールなら shell 実行でも大丈夫ですか?

A: 安全とは言えません。社内限定でも、認証済みユーザー、設定ミス、CI 実行、ジョブキュー経由の値が攻撃面になります。内向きツールほどレビューが甘くなりやすいため、むしろ共通ラッパーで厳格に縛る方が安全です。

脆弱なコードと修正例

典型的な脆弱コードと、シェルを介さない安全な書き直し例を言語別に並べてみます。

Node.js の例

// NG: ユーザー入力をそのままシェル文字列に埋め込む
// target = "127.0.0.1; rm -rf /" のような入力で任意コマンドが実行される
const { exec } = require('child_process');
const target = req.query.target;
exec(`ping -c 3 ${target}`, (err, stdout) => {
  res.send(stdout);
});
// OK: spawn で引数を配列として渡し、シェルを起動しない
const { spawn } = require('child_process');
const target = req.query.target;

// ホワイトリスト検証(IPv4 形式のみ許可)
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(target)) {
  return res.status(400).send('Invalid IP address');
}

const ping = spawn('/bin/ping', ['-c', '3', target], {
  shell: false,   // シェルを介さない(デフォルトは false だが明示する)
  timeout: 5000,  // 5秒タイムアウト
});
ping.stdout.on('data', (data) => res.write(data));
ping.on('close', () => res.end());

Python の例

# NG: shell=True でユーザー入力を渡す
import subprocess
domain = request.args.get('domain')  # "example.com; cat /etc/passwd" が来る可能性
result = subprocess.run(f"nslookup {domain}", shell=True, capture_output=True)
# OK: リストで引数を渡し shell=False(デフォルト)
import re
import subprocess

domain = request.args.get('domain', '')
# ドメイン名として有効な文字のみ許可(ホワイトリスト)
if not re.match(r'^[a-zA-Z0-9.-]{1,253}$', domain):
    return 'Invalid domain', 400

result = subprocess.run(
    ['/usr/bin/nslookup', domain],
    capture_output=True,
    text=True,
    timeout=10,   # タイムアウト必須
    env={},       # 環境変数をクリア
)

PHP の例

// NG: escapeshellarg を使っていても exec 自体がシェルを介するため原則禁止
$file = $_GET['file'];
exec("convert " . escapeshellarg($file) . " output.jpg", $output);

// OK: 許可リストで入力を絞り、ライブラリ経由で処理する
$allowed_files = ['photo1.png', 'photo2.png'];
$file = basename($_GET['file']);
if (!in_array($file, $allowed_files, true)) {
    http_response_code(400);
    die('File not allowed');
}
// ImageMagick を PHP ライブラリ(Imagick)経由で呼び出す(シェル不要)
$imagick = new Imagick('/uploads/' . $file);
$imagick->writeImage('/output/converted.jpg');

よくやらかす失敗パターンと対処法

コードレビューやインシデント対応で実際に遭遇したパターンです。

① 「管理画面だから安全」と思って検証を省いた

原因: 内部ツールや管理画面は「社内の人しか使わないから」と高をくくりがち。
対処法: 認証済みユーザーでも SSRF や誤操作で危険な入力が届くことがある。内部ツールでも同じバリデーションを適用するのが鉄則でした。

execexecFile に変えただけで安心した

原因: execFile でもファイル名部分にユーザー入力を混ぜると脆弱になる。
対処法: 実行するバイナリのパスは絶対パスのハードコードにし、引数だけを配列で渡してさらにホワイトリストで検証する。

③ 設定ファイルの値を「信頼できる入力」だと思い込んでいた

原因: .env や管理画面から保存された設定値を「ユーザー入力とは別物」と判断し、そのままコマンド引数に渡していた。
対処法: 設定値も allowlist や型・長さ検証を通してから引数に渡す。ジョブキューやスケジューラに動的な値が流れる設計は特に要注意です。

④ タイムアウトを設定していなかったためサーバーがぶら下がった

原因: 外部コマンドにタイムアウトを設定しておらず、意図的に長時間実行させられた。
対処法: 必ずタイムアウトを設定します。Python なら timeout=10、Node.js なら spawntimeout オプションを使います。

# タイムアウト付きの安全な外部コマンド実行例
try:
    result = subprocess.run(
        ['/usr/bin/dig', '+short', domain],
        capture_output=True,
        text=True,
        timeout=5,   # 5秒で強制終了
        env={}       # 環境変数をクリア
    )
except subprocess.TimeoutExpired:
    return 'Request timed out', 408

⑤ PATH 汚染に気づかなかった

原因: lscurl のようにコマンド名だけで呼ぶと、PATH が改ざんされた環境では別のバイナリが実行される。
対処法: 外部コマンドは /bin/ls/usr/bin/curl のように必ず絶対パスで指定する。さらに env={} で環境変数をクリアすると安全性がさらに上がると確認しました。

私の検証メモ

本記事は、自分が業務 / 自宅環境で実際にぶつかった事象に対し、検証用 VM やサブ機を使って再現・対処した一次記録です。一般論ではなく『私の環境では確かにこう挙動した』という観測をベースにしているため、構成が違えば挙動も変わります。再現できない場合は、本文中で挙げた前提条件(OS バージョン / ドライバ / BIOS / 関連サービスの状態)から差分を疑ってください。

マスタリングLinuxシェルスクリプト 第2版
マスタリングLinuxシェルスクリプト 第2版
シェルスクリプトの基礎から、OS コマンドインジェクションを防ぐためのセキュアな実装手法までを網羅した実践書です。外部入力を安全に扱うための引数配列指定のテクニックなど、単に動くだけでなく、堅牢なシステムを構築するための『プロの知恵』が凝縮されています。セキュリティを意識した高品質なスクリプティングを習得したいエンジニア必携の一冊です。