ファイルアップロード機能は「爆弾」を預かる行為であると心得よ
システム開発において、ユーザーにファイルをアップロードさせる機能は、往々にして「システムの急所」となる。多くのエンジニアが拡張子をチェックし、MIMEタイプを弾けば安全だと思い込んでいるが、それはセキュリティの門外漢が抱く幻想に過ぎない。
今日、攻撃者は魔法のようなテクニックを駆使する。画像ファイルに偽装したWebシェル(バックドア)をアップロードし、それがサーバー上で実行された瞬間に、君たちのシステムは攻撃者の掌中に落ちる。今日は、現場の泥臭いインシデント事例を教訓に、真に堅牢な実装とは何かを叩き込む。
—
1. 攻撃者が狙う「盲点」:なぜ従来の対策は突破されるのか
まず、よくある「失敗する実装」を理解しろ。
- MIMEタイプの盲信:
finfoやmime_content_typeはファイルの中身を解析するが、攻撃者は「画像データ+PHPコード」を連結したファイルを作成する。ライブラリが「これはJPEGだ」と判定しても、Webサーバーが実行権限を持っていれば、その中のPHPコードは平然と動き出す。 - 拡張子制限の回避: Windowsの古い仕様や、サーバー側の設定ミスを突き、
shell.php.jpgやshell.php%00.jpg(NULLバイト攻撃)のようなファイル名でフィルターをすり抜ける。 - 実行権限の未分離: ファイルを保存するディレクトリに「PHP実行権限」が残っていることが最大の罪だ。
—
2. 完璧な防御のための「3層構造」実装
防御は、単一のバリデーションではなく「多層防御」で構築する。
階層1:ファイル名のサニタイズ(PHP)
ファイル名はユーザー入力をそのまま使わない。ユニークなハッシュ値に変換し、拡張子はホワイトリストで管理せよ。
階層2:MIMEタイプの厳格な検証(Python/Flask)
python-magic ライブラリを使い、マジックナンバーまで徹底的に調べる。
import magic
def validate_image(file_stream):
# ファイルの先頭部分を読み取って真のMIMEタイプを判定
mime = magic.from_buffer(file_stream.read(2048), mime=True)
allowed_mimes = [‘image/jpeg’, ‘image/png’]
if mime not in allowed_mimes:
raise ValueError(“悪意のあるファイルと判断しました。”)
# 読み込み位置をリセットするのを忘れるな(致命的なバグの温床)
file_stream.seek(0)
—
3. インフラで止めろ:Webサーバー設定の要(Nginx)
アプリケーションのロジックが破られたときのために、インフラ側で「実行禁止」を強制する。保存用ディレクトリでは、PHPなどのスクリプト言語の実行を物理的に拒否しろ。
Nginx設定例:
ユーザーがアップロードしたファイルを置くディレクトリ
location /uploads/ {
# PHPやCGIなどの実行を完全に無効化する
location ~ \.(php|php7|phtml|pl|py|cgi)$ {
deny all;
return 403;
}
# 画像以外のコンテンツタイプをブラウザに解釈させない
add_header X-Content-Type-Options nosniff;
}
—
4. セキュリティチーフからの助言:実務で生き残るための「鉄則」
最後に、教科書には載っていない泥臭い現場の知見を授ける。
1. 保存場所はドキュメントルート外に置け: 公開用ディレクトリ(/var/www/html)にアップロード先を作ってはならない。アクセス不可能なディレクトリに保存し、配信は専用のプロキシスクリプト経由で行え。
2. クラウドストレージの活用: 自前サーバーで管理せず、AWS S3などに保存せよ。S3であれば、そもそもサーバーサイドスクリプトが実行される余地がない。これが現在もっとも推奨される構成だ。
3. ファイル名に機密情報を含めるな: ユーザーがアップロードしたファイル名に個人情報や推測可能な情報が含まれていると、IDOR(不適切な認可)の脆弱性に繋がる。必ずハッシュ化せよ。
ファイルアップロード機能は、単なる機能追加ではない。「外部の不特定多数から実行可能なコードをサーバーへ送り込む入り口」を作っているのだという危機感を持て。この実装をサボることは、君たちのシステムを攻撃者の遊び場にすることと同義だ。
実装が終わったら、必ず「Webシェルをアップロードして実行できるか」というPoC(概念実証)を自ら行え。それが、エンジニアとしての最後の砦だ。
コメント