APIの「入り口」でコマンドを仕込まれるという失態:OSコマンドインジェクションの深層と防壁設計
ネットワークの最前線に立つアーキテクトとして、これまで数多の脆弱性診断やインシデントレスポンスに関わってきたが、依然として後を絶たないのが「OSコマンドインジェクション」だ。
特に、Web APIがバックエンドのOSシェルを呼び出すような設計は、ネットワークのトランスポート層でどれほど堅牢なTLS 1.3の暗号スイートを組もうが、どれほどQUIC(HTTP/3)でRTTを削り取ろうが、アプリケーション層の一撃で全てが崩壊する。今回は、この「禁断の果実」を安全に処理するための、プロトコルスペシャリストとしての視点を共有したい。
—
1. なぜ「サニタイズ」という甘美な響きに溺れてはいけないのか
多くのエンジニアが escapeshellarg() や filter_var() に頼るが、これはネットワークのパケットフィルタリングで言えば「ブラックリスト方式」と同義だ。未知の攻撃パターン、あるいはシェルの特異な挙動(環境変数展開やトークン分割)に対しては無力である。
OSコマンドを直接実行するAPIエンドポイントは、設計の時点で「アンチパターン」と見なすべきだ。もし、どうしてもOSの機能を叩く必要があるなら、シェルを介さない execve() 系のシステムコールを直接呼び出すか、あるいは「ホワイトリスト化したトークン」によるマッピングを行うのが鉄則である。
シェルを介さない安全な呼び出し(Pythonの例)
os.system や shell=True を使った subprocess.run は厳禁だ。以下のコードのように、引数を配列として渡し、シェルをバイパスする設計を徹底せよ。
import subprocess
def run_safe_command(user_input):
# ホワイトリストによる厳格なバリデーション
allowed_commands = {"ping": "/usr/bin/ping", "traceroute": "/usr/bin/traceroute"}
if user_input not in allowed_commands:
raise ValueError("許可されていないコマンドです")
# shell=False を維持し、引数をリストとして渡す
# これにより、シェル特有の記号展開(;, |, & など)は無効化される
result = subprocess.run(
[allowed_commands[user_input], "-c", "4", "8.8.8.8"],
capture_output=True,
text=True,
check=True
)
return result.stdout
—
2. トランスポート層からアプリケーション層まで:防壁の多層化
コマンドインジェクションが成功した瞬間、攻撃者はあなたのサーバーの「シェル」を手に入れる。そこから先の横展開を防ぐために、ネットワークの観点から以下のチューニングを推奨する。
TLSハンドシェイクとHTTP/2ヘッダー圧縮の最適化
脆弱なAPIは往々にして、リクエストの正規化が甘い。HPACK や QPack を用いたヘッダー圧縮はパフォーマンスには寄与するが、巨大なヘッダーを送りつける攻撃(Slowloris系やDoS)に対しても、Webサーバー側の limit_request_header_size を絞り、不審なパケットを早期にドロップするように設定しておくこと。
システムコールレベルの制限(Seccomp/AppArmor)
万が一、アプリケーションが乗っ取られたとしても、そのプロセスから execve や socket へのアクセスを制限する。Linuxカーネルの Seccomp を利用して、必要なシステムコール以外を禁止すれば、攻撃者がバックシェルを起動しようとした瞬間にカーネルによってそのプロセスは SIGKILL される。
# AppArmorのプロファイル例:対象プロセスに許可する実行ファイルを制限する
# /etc/apparmor.d/usr.bin.my_api_app
profile my_api_app /usr/bin/my_api_app {
/usr/bin/ping ix, # pingのみ許可
deny /usr/bin/python*, # インタプリタの起動を拒否
deny /bin/sh ix, # シェルの起動を完全禁止
deny /bin/bash ix,
}
—
3. REST APIの設計哲学:ステートレスと「URLの美学」
美しいREST APIとは、単にURLが綺麗なだけではない。各リソースが「状態」を持たず、インジェクションの余地がないほどに「抽象化」されていることが重要だ。
URLの設計において、パスパラメータにコマンドそのものを含めるような設計(例: /api/v1/execute/{command})は、もはや設計の敗北と言える。代わりに、以下のような設計へリファクタリングせよ。
- 悪い例:
GET /api/v1/system/run?cmd=ping+8.8.8.8 - 良い例:
POST /api/v1/network-diagnostics - Body:
{"task": "ping", "target": "8.8.8.8"}
この設計であれば、target の中身を正規表現で IPv4/IPv6 アドレスに厳格に縛り付けることができ、コマンドそのものを入力値として受け取る必要がなくなる。
—
4. スペシャリストとしての提言
ネットワークのパケットは、送受信されるまで何が起こるか分からない。しかし、APIの入り口で入力をどう扱うかは、設計者の意志一つで完璧に制御できる。
1. 入力を信用するな: Content-Type が application/json であっても、パケットのペイロードを信頼してはならない。
2. 最小特権の原則: APIを実行するOSユーザーに sudo や root 権限を与えるなど論外だ。専用の nobody ユーザーを割り当て、chroot や Docker のコンテナ制約で外界と隔離せよ。
3. 可観測性の確保: 全てのリクエストをログに出力せよ。特に、異常な文字列(;, |, &&, $(), ` `)が含まれるリクエストは、IDS/IPSでアラートを飛ばすだけでなく、即時にセッションを切断する TCP RST` を送出するような構成にすべきだ。
プロトコルの美しさは、堅牢な防御の上に成り立つ。貴殿が構築するAPIが、攻撃者のパケットを無慈悲に拒絶し、高パフォーマンスで信頼性の高い通信を実現することを期待している。ネットワークの深淵を覗く者は、アプリケーションの細部までをも制御せねばならないのだから。
コメント