現場の最前線で火を消し続けているエンジニア諸君、お疲れ様。
今日は「教科書には載っているが、なぜか本番環境で消えない」インジェクション攻撃について、現場の視点から深掘りする。
「プリペアドステートメントを使え」という言葉を何百回聞いたか? それでもなぜ脆弱性が生まれるのか。それは、多くのエンジニアが「なぜ攻撃が成功するのか」「どうすれば逃げ場を完全に塞げるのか」のメカニズムを、自分の手で叩き込んでいないからだ。
今日は、SQLインジェクション(SQLi)とOSコマンドインジェクションを例に、泥臭い防御の真髄を共有する。
—
1. SQLインジェクションの正体:なぜ「変数」をクエリに混ぜてはいけないのか
SQLiの攻撃者は、君たちが書いた「動的なクエリ組み立て」の隙間を縫う。
例えば、ユーザーIDを受け取ってデータを検索するだけの、何の変哲もないコードを想像してくれ。
-- 攻撃者がこんな入力を投げたらどうなる?
-- ID: 1 OR 1=1 --
SELECT * FROM users WHERE id = 1 OR 1=1 --';
-- 以降がコメントアウトされ、OR 1=1 がすべての条件を真にする。結果、DB内の全データが流出する。これは基礎中の基礎だが、現役のプロジェクトでも平気で sprintf や文字列結合("id = " . $id)を使っているコードを見かける。
防御策:プリペアドステートメントという「壁」
プリペアドステートメントの真髄は、「SQL文の構造(実行計画)」と「データ(値)」を分離する点にある。クエリをコンパイルした後に値を流し込むため、攻撃者が後から値を細工しても、それはあくまで「文字列データ」としてしか解釈されない。
PHP (PDO) でのセキュア実装
<?php
// PDO接続インスタンス
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'user', 'pass');
// 1. 構造だけ先に定義。値はプレースホルダ(?)にする
$sql = "SELECT username, email FROM users WHERE id = ?";
$stmt = $pdo->prepare($sql);
// 2. 値を安全にバインドする(型を指定すれば更に堅牢)
$id = $_GET['id'];
$stmt->bindParam(1, $id, PDO::PARAM_INT); // ここで型を強制するのも重要
$stmt->execute();
$result = $stmt->fetchAll();
?>
—
2. OSコマンドインジェクション:サーバーそのものを奪われる恐怖
SQLiよりも深刻なのが、OSコマンドインジェクションだ。Webアプリ経由でサーバーのシェルを叩く。ping や imageMagick の処理でよく見かける脆弱性だ。
攻撃手法(PoC)の思考
攻撃者は 127.0.0.1; cat /etc/passwd のような入力を試す。もしアプリが exec("ping -c 1 " . $input) と書いていれば、サーバーは ping を実行した直後に、パスワードファイルの内容を吐き出すことになる。
防御の鉄則:ホワイトリスト検証
外部からの入力は、「性悪説」で扱うのが鉄則だ。
- ホワイトリスト検証: 想定される値(数字のみ、英数字のみ)以外は即座に拒絶する。
- APIの選択: 可能なら
exec()やsystem()を避け、言語組み込みのライブラリ(PythonのsubprocessやPHPのescapeshellarg())を使う。
Pythonでのセキュアな実装例
import subprocess
import re
def get_ping_result(target_ip):
# 1. ホワイトリスト形式でバリデーション(IPv4アドレス形式のみ許可)
if not re.match(r"^\d{1,3}(\.\d{1,3}){3}$", target_ip):
raise ValueError("不正なIPフォーマットです")
# 2. リスト形式で渡すことでシェル経由の解釈を防ぐ
# shell=False (デフォルト) を維持すること
result = subprocess.run(["ping", "-c", "1", target_ip], capture_output=True, text=True)
return result.stdout
—
3. 防御の「多重化」:インフラ層での締め付け
コードの修正には時間がかかることもある。その間、インフラで守る姿勢も重要だ。
Nginx/WAFでの防御(ModSecurityの例)
WAF(Web Application Firewall)を導入しているなら、明らかなインジェクションパターンを検知してブロックする設定を入れよう。
# WAFの設定例: 特定のキーワードを含むリクエストを403で返す
location /api/ {
# SQLiの典型的なキーワードを簡易検知
if ($query_string ~* "(union|select|insert|update|delete|drop|--|#)") {
return 403;
}
}
※注:これはあくまで補助的な防御だ。コードの脆弱性を修正しなくていい理由にはならない。
—
現場のリーダーから君たちへ
最後に、セキュリティの本質について一つ。
「脆弱性をなくす」こと以上に重要なのは、「いつか必ず攻撃される」という前提でシステムを設計することだ。
1. 最小権限の原則: WebアプリがDBに接続するユーザーには、DROP TABLE 権限を絶対に与えるな。SELECT, INSERT, UPDATE だけで十分なはずだ。
2. ログの監視: 異常なクエリやコマンドが実行された際、即座にアラートが上がる仕組み(SIEMやCloudWatch Logsのメトリクスフィルター)を構築しておけ。
3. 依存関係の管理: フレームワークやライブラリは常に最新に。SQLiの脆弱性は、自前のコードだけでなく、古いORMライブラリのバグから発生することもある。
セキュリティは、一度やって終わりのタスクではない。日々のコードレビューで「この変数、SQLに直接混入してないか?」と、しつこく問い続けること。その泥臭い継続こそが、君のサービスを守る最大の壁になる。
さて、次にコードを書くときは、その input が悪意に満ちていると仮定してタイピングしてくれ。それがプロの仕事だ。
コメント