OSコマンドインジェクションの墓場:API設計から排除すべき「シェル依存」という病
多くのセキュリティエンジニアが「OSコマンドインジェクションの防止」と聞くと、決まって「入力値をサニタイズ(エスケープ)しましょう」と口にする。しかし、レッドチームの視点から言わせれば、それは脆弱性を放置するための言い訳に過ぎない。
真の攻撃者は、エスケープの不備や、エンコーディングの差異(マルチバイト文字によるデリミタの破壊など)を突き、セキュリティフィルタを無力化する。OSコマンドインジェクションを撲滅するには、サニタイズという「修正」ではなく、シェルを呼び出すという「設計の欠陥」をシステムから物理的に削除するしかない。
1. exec系関数は「存在しないもの」として扱う
モダンなアプリケーション開発において、system()、exec()、passthru()、shell_exec() といった関数を使用することは、セキュリティ上の自殺行為である。これらをラップした関数をプロジェクト内で禁止するだけでは不十分だ。CI/CDパイプラインに、言語レベルの静的解析(PHPならPHPStanやPsalmのカスタムルール、PythonならBanditなど)を組み込み、これらの関数への呼び出しをビルドエラーとして検知させる必要がある。
もし外部プロセスを実行しなければならない場合、シェル経由ではなく、バイナリを直接呼び出すインターフェース(proc_openの配列引数や、Pythonのsubprocess.run(args, shell=False))を強制するべきだ。
import subprocess
# 【NG】shell=Trueはシェルを介して実行されるため、インジェクションの温床になる
# subprocess.run("ls -l " + user_input, shell=True)
# 【OK】リスト形式で渡すことで、シェルを介さず引数として安全に処理される
def safe_execute(filename):
# 引数をリストとして分離することで、メタ文字(;, |, &, `等)も単なる文字列として扱われる
cmd = ["/usr/bin/file", filename]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout
except subprocess.CalledProcessError as e:
return f"Error: {e}"
2. ホワイトリスト検証の解釈を誤るな
「入力値のホワイトリスト検証」を、単純な正規表現チェックだと考えていないだろうか。攻撃者は常に、プロトコル仕様の裏をかく。例えば、ファイル名を検証する際、単に ^[a-zA-Z0-9]+$ としても、OS側のファイルシステム(NTFSやEXT4)が解釈する特殊なデバイスパス(/dev/null や Windowsの NUL など)を通過させてしまうケースがある。
アーキテクトとして、ホワイトリストは「値の範囲」だけでなく「意味論的な妥当性」まで踏み込むべきだ。
// 【推奨されるアプローチ】
// 入力値をそのままファイル名に使わず、マッピングテーブルやUUIDで管理する
$allowed_files = [
'report_q1' => '/var/data/q1.pdf',
'report_q2' => '/var/data/q2.pdf',
];
$key = $_GET['file_id']; // ユーザー入力
if (!isset($allowed_files[$key])) {
throw new Exception("不正なアクセスです。");
}
// 実際に実行するバイナリやファイルパスは、ユーザー入力から一切生成しない
$target = $allowed_files[$key];
3. 生成AI時代のガードレイル:入力層での「意図」の分離
現在、最大の脅威はプロンプトインジェクションとOSコマンドインジェクションの融合だ。AIが生成したコードやパスが、バックエンドでOSコマンドを実行するフローが存在する場合、それはもはや「攻撃者が自由にコマンドを実行できるAPI」と化す。
これに対する防衛策は、「コンテキストの分離」である。
- 特権の最小化: アプリケーションを実行するOSユーザーは、
nologinシェルを指定し、実行可能なバイナリをchrootやDockerのコンテナ内で厳密に制限する。 - 認可の分離: OSコマンドを実行する権限を持つモジュールを、メインのAPIサーバーから独立したマイクロサービスとして切り出し、通信をUnixドメインソケット経由で限定する。
結論:防御のアーキテクチャとは「諦め」の技術である
セキュリティの極意は、「ユーザー入力を信じて安全に処理すること」を諦めることにある。OSコマンドインジェクションが起きるのは、システムがシェルに対して「ユーザーからの入力を解釈してくれ」と頼むからだ。
シェルを介さない設計、入力の完全な抽象化、そして実行環境のサンドボックス化。これらを組み合わせて、「万が一インジェクションが成功しても、何もできない」という絶望的な環境を攻撃者に突きつけることこそ、我々アーキテクトが目指すべきゴールだ。
教科書的なパッチ適用に時間を費やすのではなく、設計レベルでの「攻撃不可能性」を追求せよ。それが、レッドチームが最も嫌う防御の形である。
コメント