Path Traversalの深淵:なぜ「ファイル名だけ」を信じてはいけないのか
現場でコードレビューをしていると、未だに「$filename = $_GET['file'];」といった記述に出会うことがある。彼らは悪気なく「サーバー上の特定のディレクトリにあるファイルだけを読み込む」と信じているが、攻撃者にとってそれは「サーバーの心臓部を覗くための鍵」に他ならない。
Path Traversal(ディレクトリトラバーサル)は、最も原始的でありながら、最も確実にシステムを破壊できる脆弱性の一つだ。今日は、なぜ安易なフィルタリングが破られるのか、そして実務レベルでどう防ぐべきかを、泥臭い現実とともに解説する。
—
1. 攻撃者が「../」の先に見ているもの
Path Traversalの基本は、../(親ディレクトリへの移動)を利用して、アプリケーションが想定しているディレクトリの外部へ脱出することだ。
攻撃者はただランダムにパスを入力しているわけではない。彼らはサーバーのOS、Webサーバーの種類、そして設定ファイルをプロファイリングしている。例えば、Linux環境であれば /etc/passwd や /etc/shadow、PHP環境であれば config.php や .env ファイルが彼らの最初の標的となる。
典型的なPoCの例
GET /download.php?file=../../../../etc/passwd HTTP/1.1
Host: target-server.com
このリクエストが成功すれば、サーバー上の機密情報が露呈する。さらに危険なのは、Webサーバーのログファイルやセッションファイルを経由した「ログポイズニング」によるRCE(リモートコード実行)への発展だ。一度足掛かりを作れば、攻撃者はWebシェルをアップロードし、バックドアを設置する。
—
2. 「ブラックリスト」という名の幻想
多くのエンジニアが犯す最大の過ちが、「../ を置換して消せばいい」という考えだ。
// 危険な実装例
$file = str_replace('../', '', $_GET['file']);
これは無意味だ。攻撃者は ....// と入力すれば、置換後に再び ../ が出現するように細工する。また、OSやファイルシステムごとの挙動(Windowsの ..\ や、URLエンコードされた %2e%2e%2f)を考慮しきれない。セキュリティにおいて、「禁止リスト」は常に後手に回る。やるべきは「許可リスト」のみを通すことだ。
—
3. 鉄壁の防御:セキュアな実装パターン
ディレクトリトラバーサルを防ぐための黄金律は、「ユーザー入力をファイルパスとして直接使わない」ことだ。
Python (Flask) でのセキュアなファイル読み込み
werkzeug.utils.secure_filename を使うのが定石だが、それだけでは不十分だ。必ず「ベースディレクトリ」を固定し、絶対パスで検証する。
import os
from flask import send_from_directory, abort
BASE_DIR = '/var/www/uploads'
def get_file(filename):
# 1. 安全なファイル名に変換
safe_name = os.path.basename(filename)
# 2. 絶対パスを生成して結合
file_path = os.path.join(BASE_DIR, safe_name)
# 3. realpathでシンボリックリンクやトラバーサルを解決し、BASE_DIR配下か検証
if not os.path.abspath(file_path).startswith(os.path.abspath(BASE_DIR)):
abort(403) # 不正なアクセス
return send_from_directory(BASE_DIR, safe_name)
PHPでのセキュアな実装例
PHPでは realpath() を活用し、期待するディレクトリパスで始まるかを厳格にチェックする。
<?php
$base_dir = '/var/www/uploads/';
$requested_file = $_GET['file'];
// ファイル名をホワイトリストで管理するか、basenameでディレクトリ情報を捨てる
$filename = basename($requested_file);
$path = $base_dir . $filename;
// realpathで解決したパスがbase_dirで始まるか確認
if (realpath($path) && strpos(realpath($path), realpath($base_dir)) === 0) {
readfile($path);
} else {
// ログに記録し、403を返す
header("HTTP/1.1 403 Forbidden");
exit('アクセス権限がありません。');
}
?>
—
4. インフラ層での防御:nginxの設定
アプリケーションコードに不備があっても、インフラ側で防御層を作るのが「多層防御」の基本だ。nginxの設定で、リクエストパスに .. が含まれる場合は拒否するように設定する。
# /etc/nginx/conf.d/security.conf
# 連続するドットや、怪しいパスを正規表現でブロック
if ($query_string ~ "\.\.") {
return 403;
}
# または、locationディレクティブで特定のパス以外を厳格に制御する
location /uploads/ {
alias /var/www/uploads/;
# ファイル名に特殊文字を含ませないなどの制限をかける
location ~* \.\. {
deny all;
}
}
—
セキュリティチーフからの最後のアドバイス
Path Traversalの脆弱性は、コードが数行増えるだけで防げるものだ。しかし、最も危険なのは「自分のコードは大丈夫だ」という慢心である。
1. IDベースのアクセス: ファイルを直接パスで指定させるのではなく、DB上のID(UUIDなど)で管理し、サーバー側でマッピングする。これが最も安全だ。
2. 最小権限の原則: Webサーバーを実行しているユーザー(www-dataなど)には、アプリケーションのソースコードや /etc への読み込み権限を与えてはならない。
3. 継続的なスキャン: CI/CDパイプラインにSnykやOWASP ZAPを組み込み、自動的に「../」を使ったテストケースを実行する習慣をつけよう。
脆弱性を探すのは攻撃者の仕事だが、それを塞ぎ、盤石なシステムを構築するのはエンジニアの誇りだ。君たちが書くコードが、明日のインシデントを未然に防ぐ防壁になることを期待している。
コメント