ファイルアップロードは「毒の入り口」だ:パス・トラバーサルを根絶する実装の鉄則
現場で数々のインシデントを見てきたが、Webアプリの「ファイルアップロード機能」ほど、攻撃者にニヤリとさせる箇所はない。開発者が「単にファイルを置くだけ」と考えているその場所が、実はOSコマンド実行やWebシェル設置の最短ルートになっているからだ。
今日は、特に見過ごされがちな「パス・トラバーサル」を入り口とした攻撃を防ぐための、実戦的な防御哲学を叩き込む。
なぜ、ファイル名をそのまま保存してはいけないのか
攻撃者は、アップロード時のファイル名(Content-Dispositionヘッダ等)に細工を施す。
例えば、ファイル名を ../../../var/www/html/shell.php と書き換えて送る。アプリ側がこれをそのまま受け取れば、アップロード先のディレクトリを突き抜け、Web公開領域に任意のスクリプトを配置できてしまう。これがパス・トラバーサルの悪夢だ。
攻撃者の視点:PoC(概念実証)のイメージ
1. 攻撃: filename="../../../var/www/html/shell.php" というリクエストを送信。
2. 脆弱性: アプリが base_path + user_input_filename のように結合して save() を実行。
3. 結果: 本来のアップロード先ではなく、任意のディレクトリに悪意あるファイルが着弾する。
4. 追撃: 攻撃者はブラウザからそのURLにアクセスし、OSコマンドを実行する。
これに対する「ファイル名にサニタイズをかける」という対策は、泥縄式だ。ブラックリスト方式でのフィルタリングは、OSごとの特殊文字やエンコーディングの差異を突かれて簡単に突破される。
鉄壁の防衛:UUIDとホワイトリストによる「完全分離」
防御の原則はシンプルだ。「ユーザーからの入力をファイル名として信用しない」。この一点に尽きる。
1. アプリケーション層での対策(Python/Flaskの例)
ファイル名はUUIDでランダム生成し、拡張子は許可されたリストからのみ抽出する。これだけでパス・トラバーサルは論理的に不可能になる。
import os
import uuid
from werkzeug.utils import secure_filename
許可する拡張子のホワイトリスト(これ以外は受け付けない)
ALLOWED_EXTENSIONS = {‘png’, ‘jpg’, ‘jpeg’, ‘gif’}
def save_uploaded_file(file):
# 1. 拡張子のチェック
filename = file.filename
ext = filename.rsplit(‘.’, 1)[1].lower() if ‘.’ in filename else ”
if ext not in ALLOWED_EXTENSIONS:
raise ValueError(“不適切なファイル形式です”)
# 2. ファイル名をUUIDに完全置換
# 元のファイル名はDBに保存し、ディスク上の保存名はランダムにする
safe_filename = f”{uuid.uuid4()}.{ext}”
# 3. 保存先をハードコードし、結合を避ける
save_path = os.path.join(‘/var/www/uploads’, safe_filename)
file.save(save_path)
return safe_filename
2. インフラ層での対策(Nginx)
アプリケーションに万が一のバグがあった場合でも、アップロードディレクトリ内でのスクリプト実行を許してはならない。Nginxの設定で「実行権限の剥奪」を強制する。
アップロードディレクトリへのアクセス設定
location /uploads/ {
# 静的ファイルとしてのみ配信し、PHP等のインタプリタを通さない
location ~ \.(php|php7|phtml|py|pl|sh)$ {
deny all;
}
# MIMEタイプを強制的に設定し、HTMLとしての実行を防ぐ
default_type application/octet-stream;
add_header Content-Disposition “attachment”;
}
実務上のTips:脆弱性を残さないために
- 保存先ディレクトリをWeb公開ルートから外す:
可能であれば、/var/www/html/uploads のような公開領域ではなく、Webサーバがアクセス可能な別領域(例: /var/data/uploads)に保存し、配信はプロキシ経由で行うのがベストだ。
- DBとの紐付け:
ファイル名は「元の名前(ユーザー表示用)」と「UUID(ストレージ用)」をDB上で明確に分けること。ファイルを取得する際はUUIDをキーにしてストレージから読み出す。
- クラウドストレージ(S3等)の活用:
可能ならローカルファイルシステムを使わず、S3等のオブジェクトストレージに逃がす。その際も、キー(ファイル名)は必ずUUID等の予測不可能な文字列にすること。
まとめ:防御の心得
セキュリティとは、単なる「技術の詰め合わせ」ではない。「ユーザー入力は常に悪意があるものと仮定し、システム構造自体でその悪意を無効化する」という設計思想そのものだ。
「ファイル名をきれいに置換すれば大丈夫だろう」という慢心が、大規模インシデントの引き金になる。今日からコードを見直し、UUIDでのリネームと拡張子の厳格なホワイトリスト化が徹底されているか確認してほしい。
もし君たちのシステムで、「アップロード機能のファイル名処理が怪しい」と感じたなら、迷わずリファクタリングの時間を確保すること。それが、将来の自分を、そして何よりユーザーを守ることにつながるのだから。
コメント