utsuroi path traversal labs Lab 05 / 5 — Practitioner
05

prefix チェックの不備 — 末尾スラッシュ忘れ

Path traversal via missing trailing slash in prefix check

Lab 02〜04 を経て、うつろい journal は「正規化 + 境界チェック」アプローチを採用した。これで ../ 系の攻撃は全て防げるはず:

// PHP - realpath で正規化、prefix チェックで境界確認
$base = '/var/www/utsuroi/articles';   // ← 末尾スラッシュなし
$file = $_GET['file'];
$resolved = realpath($base . '/' . $file);

if ($resolved === false) abort(404);

// ★ バグ:末尾スラッシュ無しの prefix チェック
if (strpos($resolved, $base) !== 0) {
    abort(403, 'Outside base directory');
}

return readfile($resolved);

意図は「$resolved/var/www/utsuroi/articles/ 配下にあるか」。しかし $base に末尾スラッシュが無いので、/var/www/utsuroi/articles_evil/ のような兄弟ディレクトリも prefix 一致で通ってしまう

末尾スラッシュ漏れを突いて、articles_evil/ 配下の機密ファイルを取得してください。

/var/www/utsuroi/ ├── articles/ ← 基準ディレクトリ($base、末尾スラッシュ無しで保管) │ ├── 2025-04-skincare.pdf │ ├── 2025-05-makeup.pdf │ └── 2025-06-fragrance.pdf └── articles_evil/ ← 別ディレクトリ。攻撃者は読めるはずがない…? ├── internal-prices.csv ← 卸売原価の機密表 └── unreleased-campaign.md ← 未公開キャンペーン情報
クイック試行:
処理トレース
入力: (空)
↓ realpath で正規化
↓ prefix チェック
↓ ファイル読み込み
(まだファイルを取得していません)
バグの本質
$base = '/var/www/utsuroi/articles'         // 末尾 / なし
$resolved = '/var/www/utsuroi/articles_evil/internal-prices.csv'

strpos($resolved, $base) === 0 → true
                                  ↑ articles_evil の頭に 'articles' が含まれるので素通り

正しい比較:
$resolved が '$base/' で始まるか
'/var/www/utsuroi/articles_evil/...' は '/var/www/utsuroi/articles/' では始まらない → 拒否
同種のミス
// Node.js でも同じ罠
if (resolved.startsWith(base)) {  // ← base に末尾 / 必須
  fs.readFile(resolved);
}

// Python
if resolved.startswith(base):  # ← os.sep を末尾に付ける必要あり
    open(resolved).read()
正解(ネタバレ)

ペイロード:../articles_evil/internal-prices.csv または ../articles_evil/unreleased-campaign.md

realpath は /var/www/utsuroi/articles_evil/... を返す。これは strpos での prefix チェック(末尾 / なし)を通過してしまう。../ 自体が拒否されないのは、正規化後のパスが「親ディレクトリ」に対して有効だから(articles の親は utsuroi、その下に articles_evil が存在する)。