【実務・中級編】OSコマンド実行時の環境変数汚染リスク – アプリケーションセキュリティ & 安全な開発防御ガイド

「フルパスを書けば安全」は半分正解。環境変数汚染が招くシステム乗っ取りのリアル

現場でコードレビューをしていると、必ず一度は議論になるのが「OSコマンド実行時のセキュリティ」だ。多くのエンジニアが「exec() や system() に渡す引数をエスケープすればいいんだろ?」と考える。だが、真のセキュリティプロフェッショナルは、引数の洗浄だけでなく、「実行環境そのもの」が攻撃者に汚染されている可能性を常に疑う。

今日は、多くのエンジニアが盲点とする「PATH環境変数を悪用したバイナリすり替え」について、現場のインシデントハンドリングの視点から解説する。

—

1. なぜ「コマンド名」だけでの実行が危険なのか

例えば、PHPで exec('ls -l /var/www/html'); のように書いたとする。OSは実行時、環境変数 PATH を順に参照して ls というバイナリを探しに行く。

もし、攻撃者が何らかの脆弱性を突いて環境変数を書き換えられたらどうなるか?

攻撃シナリオ:PATHハイジャック(PoC)

攻撃者は、Webサーバ内の書き込み可能なディレクトリ(/tmp 等)に、悪意ある ls という名前の実行ファイルを配置する。そして、環境変数 PATH を /tmp:/usr/bin:/bin に書き換える。

この状態でアプリケーションが exec('ls ...') を叩くと、OSは /usr/bin/ls ではなく、攻撃者が仕込んだ /tmp/ls を実行してしまう。これが環境変数汚染によるバイナリすり替えだ。バックドアを仕込むのに、これほど簡単な方法はない。

—

2. 対策の鉄則:絶対パス指定と環境変数の隔離

この攻撃を防ぐための第一歩は、「コマンドのフルパス指定」だ。/bin/ls と明示すれば、PATH を書き換えられても /tmp/ls が実行されることはない。

だが、これだけで安心するのは早い。「フルパス」を指定しても、そのプログラム自体がさらに別の外部コマンドを呼び出している場合、その内部で再び PATH が悪用されるリスクが残る。

実践:セキュアなコマンド実行の実装例

Pythonでの実装例

subprocess モジュールを使う際は、shell=True を避け、コマンドをリスト形式で渡すのが鉄則だ。さらに、環境変数を明示的にクリーンアップする。

import subprocess
import os

def secure_execute(file_path):
# 1. 実行するコマンドは必ずフルパスで指定する
cmd = [“/usr/bin/convert”, file_path, “/tmp/output.jpg”]

# 2. 環境変数を必要最小限に制限(PATHをクリアまたは最小化)
# これにより、サブプロセスが勝手に外部バイナリを探しに行くのを防ぐ
safe_env = {“PATH”: “/usr/bin:/bin”}

try:
# shell=False (デフォルト) を維持し、引数をリストとして渡すことで
# コマンドインジェクションを物理的に遮断する
result = subprocess.run(
cmd,
env=safe_env,
check=True,
capture_output=True,
text=True
)
return result.stdout
except subprocess.CalledProcessError as e:
# エラーログにはコマンドの引数をそのまま出さない(ログインジェクション対策)
print(f”Command execution failed.”)
return None

Node.js (Child Process) の場合

const { execFile } = require(‘child_process’);

// execではなくexecFileを使う。execはシェルを経由するため危険度が高い
execFile(‘/usr/bin/whoami’, [], {
env: { PATH: ‘/usr/bin:/bin’ }, // 環境変数を極限まで絞る
timeout: 5000 // タイムアウト設定も必須(DoS対策)
}, (error, stdout, stderr) => {
if (error) {
console.error(‘Execution error’);
return;
}
console.log(stdout);
});

—

3. インフラ・コンテナレベルでの防衛

コードレベルでの対策を補完するため、インフラ側でも「防御の多層化」を行う必要がある。

  • コンテナ内のPATH制限:

Dockerfileで環境変数を明示的にセットする。

# 不要なパスを通さない
ENV PATH=”/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”

  • AppArmor / SELinux:

Webサーバのプロセス(www-data 等)が、特定のディレクトリ以外(例:/tmp)で実行ファイルを実行できないよう、強制アクセス制御(MAC)を適用する。これが最後の砦だ。

—

チーフエンジニアからのアドバイス

「動けばいい」というコードは、数年後に必ず誰かの首を絞めることになる。
特にOSコマンドの実行は、アプリケーションとOSの境界線であり、ここを疎かにすることは、家の玄関に鍵をかけずに外出するのと同じだ。

1. shell=True や system() は原則禁止。
2. コマンドは必ず /usr/bin/ のようなフルパスで書く。
3. 実行時の環境変数は必要最小限に絞る。

この3つをチームのコーディング規約に盛り込むだけで、君たちのサービスを守る強固なガードレールになる。泥臭いけれど、こういう積み重ねが、インシデントゼロの平和な運用を支えるんだ。さあ、今すぐレポジトリの grep をかけて、危険な呼び出しがないか確認してくれ。

コメント

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