現場のエンジニアへ告ぐ:OSコマンドインジェクションは「フィルタリング」では防げない
「escapeshellarg() を使っているから大丈夫ですよね?」
コードレビューで後輩からこう聞かれるたび、私はいつも胃が痛くなる思いをします。残念ながら、OSコマンドインジェクションにおいて、メタ文字を無害化しようとする発想そのものが、すでに敗北の始まりです。
ブラックリスト方式でのフィルタリングは、攻撃者が新しいエスケープ手法や環境依存の挙動を見つけるたびに、あっけなく突破されます。我々が守るべきは「綺麗なコード」ではなく「現実的な防御」です。今日は、泥臭いインシデント現場で培った、OSコマンドインジェクションを根絶するための「鉄則」を共有します。
—
1. 攻撃者が狙う「シェルの気まぐれ」
OSコマンドインジェクションの恐ろしさは、単なる文字列の挿入ではありません。system() や exec() が内部でシェル(/bin/sh など)を呼び出す際、入力値が「コマンドの引数」としてではなく「コマンドそのもの」として解釈されてしまう点にあります。
例えば、ユーザーの入力値 file_name を使ってファイルを表示するこんなコード。
// 脆弱な例:シェルを介して実行される
$file = $_GET[‘file’];
system(“cat /var/www/uploads/” . $file);
もし攻撃者が file パラメータに test.txt; rm -rf / を渡したらどうなるか。シェルはセミコロン(;)をコマンドの区切りと見なし、cat の後に最悪なコマンドを実行します。escapeshellarg() で囲ったとしても、環境変数やOSのバージョン、あるいはバッククォートの処理順序によって、「想定外の挙動」を引き出すのが攻撃者の腕の見せ所です。
—
2. 対策の極意:シェルを「殺す」
最も確実な防御策は、「シェルを経由させない」こと、そして「OSの実行機能そのものを極力使わない」ことです。
A. API/ライブラリで代替する
多くの場合、OSコマンドを実行する必要はありません。ファイルを読み込むなら言語標準の関数を使い、プロセス制御が必要なら exec() ではなく、シェルを介さない関数を利用します。
PHPでの改善例(systemを使わない)
$file = $_GET[‘file’];
// ホワイトリストで許可されたファイル名のみを受け入れる
$allowed_files = [‘report.pdf’, ‘data.csv’];
if (!in_array($file, $allowed_files, true)) {
die(“不正なアクセスです。”);
}
// file_get_contentsを使うことで、シェルを介さず安全に読み込む
echo file_get_contents(“/var/www/uploads/” . $file);
B. どうしてもコマンド実行が必要な場合(ホワイトリストと引数分離)
どうしてもシェルコマンドが必要なら、exec() の引数を配列で渡す proc_open や、言語ごとの「引数分離実行」を利用してください。これなら、メタ文字はただの「文字列」として扱われます。
Pythonでの改善例(subprocess.runの安全な利用)
import subprocess
def get_file_info(file_name):
# 引数をリスト形式で渡す。これでシェルを介さない
# shell=False (デフォルト) を明示することが極めて重要
try:
result = subprocess.run(
[“/usr/bin/ls”, “-l”, “/var/www/uploads/” + file_name],
capture_output=True,
text=True,
check=True,
shell=False
)
return result.stdout
except subprocess.CalledProcessError:
return “エラーが発生しました”
—
3. インフラレイヤーでの「最後の砦」
アプリケーションコードの修正が間に合わない場合、あるいは多層防御の一環として、OSレベルで権限を剥奪しておくことが重要です。
IAM・パーミッションによる制限
Webアプリケーションが動くユーザー(例:www-data)には、必要最低限の権限しか与えてはなりません。
- 実行権限の削除:
bin/shやbashを直接実行させないよう、sudo権限を厳密に管理する。 - chroot/コンテナ化: アプリケーションを隔離された環境(Dockerコンテナなど)で走らせ、ルートディレクトリの書き込み権限を剥奪する。
WAFでの防御設定(Nginx/ModSecurity例)
WAFは「気休め」と言われることもありますが、攻撃の入り口を絞るには有効です。&, ;, |, |, , $()` といったシェルメタ文字をリクエストパラメータから検知するルールを入れましょう。
ModSecurityの疑似的な検知ルール例
SecRule ARGS “@rx [;&|`$]” \
“id:10001,phase:2,deny,status:403,msg:’OS Command Injection Attempt'”
—
結論:セキュリティは「逃げ道」を塞ぐこと
OSコマンドインジェクション対策の答えはシンプルです。
1. シェルを使わないこと。
2. どうしても使うなら、文字列連結をせず、引数を配列で渡すこと。
3. 入力を信じず、期待される値(ホワイトリスト)のみを許可すること。
「エスケープすれば大丈夫」という甘えは、システムを破壊する脆弱性に直結します。今日からコードを見直してみてください。もし system() や exec() を見つけたら、それは「リファクタリングのチャンス」です。
セキュアな設計は、一朝一夕には成し遂げられません。しかし、こうした小さな積み重ねが、将来の巨大なインシデントを未然に防ぐ「最高の一手」になることを忘れないでください。
コメント