1. .. フィルタの回避 #dot-filter-bypass
ナイーブな実装では、ユーザ入力から .. を文字列置換で削除しようとします。これは多くの場合、無効化されます。
// 脆弱な実装
$file = str_replace('..', '', $_GET['file']);
return readfile('/var/www/utsuroi/public/images/' . $file);
攻撃者は次のような入力で回避:
....//→ str_replace で..が消えて../に..././→ 1度の置換では完全には消えない....\/\/→ バックスラッシュが含まれる場合の解釈差
一度の文字列置換は、置換後に再度 .. が出現する場合に対処できない。「再帰的に消す」「.. を含むなら拒否」が最低限必要。
2. URL エンコード回避 #url-encoding
%2e%2e%2f = ../ としてサーバが解釈する。
URL エンコードでは、. は %2e、/ は %2f に対応します。フィルタが「生の ..」だけをチェックしている場合、エンコードされた形で素通りできます。
// 攻撃ペイロード ?file=%2e%2e%2f%2e%2e%2fetc%2fpasswd // サーバの URL デコード後 ?file=../../etc/passwd
Web サーバ(nginx、Apache)はリクエストパース時に URL デコードを行うので、フィルタが「リクエストの生文字列」を見ているか「デコード後」を見ているかで、攻撃成立条件が変わります。
3. 二重エンコード #double-encoding
URL デコードを1回適用後にチェックする実装に対して、二重にエンコードしたペイロードを送ると、最初のデコードでは .. に戻らず、後続の処理でデコードが追加で走った場合に攻撃成立します。
// 通常のエンコード %2e%2e%2f → デコード1回 → ../ // 二重エンコード %252e%252e%252f → デコード1回 → %2e%2e%2f → デコード2回目 → ../
Web サーバとアプリケーションが独立にデコードする場合、それぞれの段階を経て最終的に ../ に戻る。「フィルタ通過後にもう一度デコード」というパスを攻撃者は探します。
4. null バイト攻撃 #null-byte
PHP 5.3 以前や、低レベル言語(C/C++)で書かれたモジュールでは、null バイト(\0、URL 表記 %00)が文字列の終端として扱われます。
// 拡張子チェックを通すための古典的攻撃 ?file=../../../etc/passwd%00.jpg // PHP のチェック: 末尾が .jpg で終わっているので OK // ファイル読み込み: null バイトで切れて /etc/passwd を読む
現代の主要な言語実装ではほぼ修正されていますが、レガシーシステム、組込み機器、古いモジュールではまだ通用する場合があります。
5. 絶対パスでの回避 #absolute-bypass
.. を使わず、最初から絶対パスを送る。
.. をフィルタしているが、絶対パスが渡されたときの挙動を考えていない実装に対する攻撃。
// 想定: ベースディレクトリの下のファイルを読む
$base = '/var/www/utsuroi/public/images/';
$file = $_GET['file'];
$path = $base . $file;
// 攻撃者が /etc/passwd(絶対パス)を送ると…
$path = '/var/www/utsuroi/public/images/' . '/etc/passwd';
// = '/var/www/utsuroi/public/images//etc/passwd'
上の例では基準ディレクトリの中に閉じ込められますが、PHP の realpath、file_get_contents 等の関数によっては、絶対パスが指定されたら基準を無視するものがあります。
6. 正しい防御 #proper-defense
Path Traversal の根本対策は、ユーザ入力をそのままファイルパスとして使わないこと。代替の設計を3つ示します:
戦略 A: ホワイトリストで ID 化
// 許可されたファイルのマッピングを持つ
$allowed_files = [
'cat' => 'cat.jpg',
'flower' => 'flower.jpg',
'sea' => 'sea.jpg'
];
$key = $_GET['file'];
if (!isset($allowed_files[$key])) {
abort(404);
}
$filename = $allowed_files[$key];
return readfile('/var/www/utsuroi/public/images/' . $filename);
戦略 B: パス正規化と検証
$base = realpath('/var/www/utsuroi/public/images/');
$path = realpath($base . '/' . $_GET['file']);
// 正規化後のパスが基準ディレクトリの下にあるか確認
if ($path === false || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) {
abort(403);
}
return readfile($path);
realpath はシンボリックリンクや .. を解決して絶対パスを返すので、これで基準ディレクトリの中にあるかをチェックできます。
戦略 C: ファイル名のみを許可(ディレクトリ区切り文字を全拒否)
$file = $_GET['file'];
// 英数字とハイフン・アンダースコアと拡張子のみ許可
if (!preg_match('/^[a-zA-Z0-9_-]+\.(jpg|png|gif)$/', $file)) {
abort(400);
}
return readfile('/var/www/utsuroi/public/images/' . $file);
最も厳格で、設計時にファイル名のフォーマットを規定できる場合に推奨。
.. を消す」アプローチを避ける理由
文字列フィルタは、エンコードや変則表現の組み合わせで無数に回避手法があります(..%2f、....//、..%c0%af、..%252f、null バイト等)。「攻撃文字列を消す」のではなく「許可された値だけを通す」のがパスの扱いの基本原則です。