utsuroi path traversal labs 知識ベース 02 / 02 — 検証回避と防御
Bypass Techniques & Defense · Knowledge Base
02

検証回避テクニックと防御

When naive sanitization fails, and how to defend properly

Path Traversal の「対策」として「.. をフィルタする」と書く実装は多いですが、これだけでは無数の回避手法に晒されます。本章では典型的な回避テクニックと、それを上回る正しい防御を扱います。

1. .. フィルタの回避 #dot-filter-bypass

「..」を消すだけだと、形を変えれば素通り。

ナイーブな実装では、ユーザ入力から .. を文字列置換で削除しようとします。これは多くの場合、無効化されます。

// 脆弱な実装
$file = str_replace('..', '', $_GET['file']);
return readfile('/var/www/utsuroi/public/images/' . $file);

攻撃者は次のような入力で回避:

一度の文字列置換は、置換後に再度 .. が出現する場合に対処できない。「再帰的に消す」「.. を含むなら拒否」が最低限必要。

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

古典的な C 言語文字列の終端で攻撃。

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 の realpathfile_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 バイト等)。「攻撃文字列を消す」のではなく「許可された値だけを通す」のがパスの扱いの基本原則です。