1. なぜ Upload は危険か #what-is
アップロードされたファイルが Web 公開ディレクトリに置かれて、サーバ側で実行可能な拡張子(.php、.jsp、.aspx 等)で保存されると、攻撃者がそのファイルを HTTP で要求することで任意コードが実行されます。
// 攻撃者が webshell.php をアップロード <?php system($_GET['cmd']); ?> // /var/www/uploads/webshell.php に保存 // 攻撃者がアクセス: https://shop.utsuroi.example.com/uploads/webshell.php?cmd=cat /etc/passwd // → サーバ上で任意コマンド実行
2. 拡張子チェック回避 #extension-tricks
- 大文字混在 —
.PhP、.PHPが区別なく実行される(ファイルシステムが case-insensitive) - レアな拡張子 —
.phtml、.phar、.php5、.php7も Apache の設定次第で PHP として実行 - 二重拡張子 —
shell.php.jpg:Apache のAddHandler設定によっては.php部分で PHP として処理される - null バイト —
shell.php%00.jpg:言語/OS によっては null バイト以降が無視される - ディレクトリ名 — Windows の
shell.php::$DATAや、URL の trailing dot/space
3. Content-Type / MIME の偽装 #mime-type
クライアントが送る Content-Type ヘッダは攻撃者が自由に書ける。サーバ側でこれだけを見て検証するのは脆弱:
if ($_FILES['file']['type'] !== 'image/jpeg') abort(400);
Burp Suite で簡単に Content-Type: image/jpeg に書き換えられる。Content-Type は信用できない。
4. マジックバイトの偽装 #magic-bytes
サーバが「ファイル先頭が JPEG マジックバイト(FF D8 FF)で始まるか」をチェックしている場合、攻撃者はJPEG ヘッダの後に PHP コードを連結する Polyglot ファイルを作る。
FF D8 FF E0 ... [JPEG header bytes] ... <?php system($_GET['cmd']); ?>
これは file コマンドや getimagesize() でも JPEG として認識されることがあるが、.php として保存されれば PHP インタプリタが処理し、HTML/PHP 部分が実行される。
5. 正しい防御 #defense
- ファイル名は再生成 — 元のファイル名・拡張子を使わず、UUID + 検証済み拡張子で保存(例:
{uuid}.jpg) - ホワイトリスト拡張子 — 許可リストにある拡張子のみ。「.php を拒否」というブラックリストは漏れが必ず出る
- 実行権限を切る保存先 — アップロード先ディレクトリで PHP/CGI 実行を無効化(Apache の
php_admin_value engine offや、CDN/オブジェクトストレージへの保存) - 別ドメイン/別オリジン — アップロードファイルを
uploads.utsuroi.example.com等の別ドメインに置く。万一実行されても本体ドメインの Cookie 等が窃取されない - マジックバイト検証 + 画像再エンコード — ImageMagick 等で再エンコード。これで PHP コードを末尾に付けた Polyglot ファイルでもコード部分が捨てられる
- Content-Type 検証は補助のみ — クライアント送信値は信用しない