【実務・中級編】 Path Traversal (ディレクトリトラバーサル) の防御とフィルタリング – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

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を組み込み、自動的に「../」を使ったテストケースを実行する習慣をつけよう。

脆弱性を探すのは攻撃者の仕事だが、それを塞ぎ、盤石なシステムを構築するのはエンジニアの誇りだ。君たちが書くコードが、明日のインシデントを未然に防ぐ防壁になることを期待している。

コメント

タイトルとURLをコピーしました