パストラバーサルとは
パストラバーサル(Path Traversal)は、別名 ディレクトリトラバーサルとも呼ばれ、ユーザー入力で指定されたファイルパスに ../(親ディレクトリへの遷移)を含めることで、本来アクセスできないはずのファイルを読み書きさせる攻撃です。
OWASP Top 10 2021 では A01:2021 - Broken Access Control の代表例に分類されます。意外と古くから知られている攻撃ですが、ファイルアップロード機能、テンプレートエンジン、ZIP展開処理など、ファイルパスを扱うあらゆる箇所で今でも見つかります。
典型的な脆弱なコード
# 脆弱な例(Python)
@app.route("/files/<filename>")
def serve_file(filename):
path = os.path.join("/var/www/uploads", filename)
return open(path).read() # ← パス検証なし攻撃者は次の URL を送ります:
GET /files/../../../../etc/passwd
GET /files/..%2f..%2f..%2fetc%2fpasswd # URLエンコード
GET /files/..%252f..%252fetc%252fpasswd # ダブルエンコード
GET /files/....//....//etc/passwd # ../を二重に書く
GET /files/..\..\..\windows\win.ini # Windows用バックスラッシュサーバー側では os.path.join("/var/www/uploads", "../../../../etc/passwd") が "/etc/passwd" に解決され、システム全体のファイルが読まれます。Linux の /etc/passwd や /proc/self/environ、Windows の win.ini や boot.ini が典型的な検証ターゲットです。
検査回避の典型パターン
| テクニック | 例 | 狙い |
|---|---|---|
| URLエンコード | %2e%2e%2f | 文字列 ../ の単純検査を回避 |
| ダブルエンコード | %252e%252e%252f | 1回デコードしてから検査するコードを回避 |
| UTF-8 オーバーロング | %c0%ae%c0%ae/ | 非正規UTF-8シーケンス(旧IIS脆弱性で有名) |
| 16bit Unicode | %u002e%u002e%u002f | IIS の独自デコードを利用 |
| NULL バイト | file.txt%00.png | C言語系で %00 以降を無視させる(Java 7以前、PHP) |
| 二重 ../ | ....// | ../ を削除する単純フィルタを回避 |
| 絶対パス | /etc/passwd | ../ を使わず直接アクセス |
URL エンコードの実際の変換は URL Encoder / Decoder で確認できます。例えば ../ → %2e%2e%2f、ダブルエンコードで %252e%252e%252f のように二段階で確認できます。
派生攻撃: ZipSlip
ZIP ファイルやTAR を展開する処理でも、エントリ名に ../ が含まれていれば任意のパスへ書き出せます。これは ZipSlipと呼ばれ、2018年に Snyk によって多数のライブラリで脆弱性が報告されました。
# 危険な ZIP のエントリ例
../../../etc/cron.d/malicious # cron に書き込み
../../../root/.ssh/authorized_keys # SSH 公開鍵を上書き対策は ZIP 解凍時に各エントリの解決後パスがベースディレクトリ配下であることを必ず検証することです。
派生攻撃: パス Confusion(テンプレート / Include)
テンプレートエンジンや require/include でユーザー入力をパスに使う場合、パストラバーサルから一段進んだ LFI(Local File Inclusion)や RFI(Remote File Inclusion)に発展します。PHP の include $_GET['page'] はその代表で、任意ファイルの読み込み+場合によってはコード実行に直結します。
防御1: 正規化後の許可リスト
最も確実な防御は、「パスを正規化(canonicalize)してから、許可ディレクトリ配下にあるかを確認」するパターンです。
# Python の安全実装例
import os
BASE_DIR = os.path.realpath("/var/www/uploads")
def safe_read(filename: str) -> bytes:
requested = os.path.realpath(os.path.join(BASE_DIR, filename))
if not requested.startswith(BASE_DIR + os.sep):
raise PermissionError("Path traversal detected")
return open(requested, "rb").read()ポイントは os.path.realpath() でシンボリックリンク・../ をすべて解決した 絶対パスを取得してから、ベースディレクトリの prefix チェックを行うこと。os.path.join() だけでは ../ 解決が行われないため不十分です。
防御2: ファイル名のホワイトリスト化
そもそも任意の文字列をパスに使わせず、「ID から内部マッピングでファイルパスを引く」設計にすると、パストラバーサルの余地がなくなります。
# 良い設計: IDからDB引きでパスを決定
@app.route("/files/<int:file_id>")
def serve_file(file_id):
record = db.files.find_one({"id": file_id, "owner": current_user.id})
if not record:
abort(404)
return send_from_directory(UPLOAD_DIR, record["storage_name"])storage_name はサーバー側で生成した UUID やハッシュにしておけば、ユーザーが任意のパスを指定する余地は一切ありません。
防御3: 抽象化APIの利用
フレームワークが提供する安全なファイル送信関数を使うのも有効です:
- Flask の
send_from_directory(): パストラバーサル検証を内部で実施 - Express の
res.sendFile():rootオプション併用でディレクトリ脱出を防止 - ASP.NET の
VirtualPathUtility.IsAppRelative(): アプリ相対パスのみを許容 - Rails の
send_file::url_based_filename等で安全化
自前で open()/fopen() を呼ぶよりも、フレームワーク提供の API を経由するのが安全です。
防御4: 実行ユーザーの権限最小化
どれだけアプリ層で防御しても完璧ではありません。「万一突破されても被害が限定される」ように、アプリ実行ユーザーの権限を最小化します:
- Web サーバーは
nobodyや専用ユーザーで動かす(root 厳禁) - 必要なファイル以外を chmod 600 で root 所有にする
- コンテナで動かす(root のままでもホストへの影響は限定的)
- SELinux/AppArmor で読み取り可能パスを制限
- chroot やコンテナで
/etc/passwd等にアクセスできなくする
実装チェックリスト
- ユーザー入力のパスを直接
open()に渡していないか - パスを 正規化(realpath / canonicalize)してから許可ディレクトリ配下か検査しているか
- URL エンコード・ダブルエンコード・NULL バイトを意識して検査しているか
- 可能なら「ID → DB → パス」の間接参照に置き換えているか
- ZIP/TAR 展開時、エントリパスがベース配下か検証しているか
- ファイル送信はフレームワーク提供の安全関数を使っているか
- アプリ実行ユーザーの権限を最小化しているか
まとめ
パストラバーサルは、Web 黎明期から存在しながら現在も繰り返し発見される根強い脆弱性です。../ 文字列を単純にブロックするだけでは URL エンコード・ダブルエンコード・NULL バイトなどで簡単に回避されます。
本質的な対策は 「パスを正規化してから許可ディレクトリ配下か検証する」か、そもそも 「ユーザーに任意の文字列を渡させず、ID 経由で間接参照する」設計に切り替えること。組み合わせれば、たとえ検査ロジックに穴があっても被害は限定されます。
URL エンコードの確認には URL Encoder / Decoder、関連リスクは OWASP Top 10 入門 も合わせてご覧ください。