はじめに
アップロードフォームを使えば、ユーザは自由にファイルをサーバに送信できます。しかし、送られてくるファイル名・拡張子・タイプ情報は、すべてユーザ側の情報。つまり、悪意を持って簡単に「偽装」できてしまうのです。
「画像ファイルに見せかけた実行ファイル」などを防ぐには、サーバ側で内容を検証し、安全に保存する仕組みが不可欠です。
目次
■ ファイルアップロードの流れと落とし穴
PHPでは、ファイルアップロードフォームを作ると以下のように $_FILES
配列に情報が入ります。
$_FILES['userfile'] = [
'name' => 'cat.png',
'type' => 'image/png',
'tmp_name' => '/tmp/phpXXXX.tmp',
'error' => 0,
'size' => 12345
];
🧩 ASCIIアート図解:「ブラウザ → PHPサーバ」構造イメージ
┌──────────────────────┐
│ ユーザのブラウザ │
│ ────────────── │
│ <input type="file"> │
│ name="userfile" │
└──────────────────────┘
│ アップロード
▼
┌───────────────────────┐
│ PHPサーバ側($_FILES) │
│ ┌────────────────┐ │
│ │ name: "cat.png" │ ← ❌ 元のファイル名(信用できない)
│ │ type: "image/png" │ ← ❌ クライアント指定(偽装可能)
│ │ tmp_name: "/tmp/..."│ ← ✅ 実際に保存された場所
│ └──────────────────┘ │
└───────────────────────┘
ここで注意すべきは、name
や type
が ブラウザ依存の情報だということ。悪意あるユーザは簡単に偽装して送ることができます。
■ よくある誤解:「$_FILES[‘type’] でチェックすればOK」?
実はこれ、誤りです。$_FILES['type']
は ブラウザが送る HTTP ヘッダの値であり、送信ツールを使えば簡単に任意の値をセットできます。
例えば──
本当の中身:PHPスクリプト
偽装ヘッダ:Content-Type: image/jpeg
これでも $_FILES['type']
は "image/jpeg"
と見えてしまいます。
つまり、サーバ側で再チェックしないと危険です。
■ 安全なチェック方法(実務でも使える)
$_FILES['userfile']['error']
がUPLOAD_ERR_OK
か確認is_uploaded_file()
で正規アップロードかチェックfinfo_file()
で MIME タイプを再判定- 許可MIME(ホワイトリスト)と照合
- 拡張子はサーバ側で付与(元の
name
を使わない) move_uploaded_file()
で保存- 保存先は
webroot
の外がベスト
💡 コード例(安全版)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['userfile']['tmp_name']);
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
if (!isset($allowed[$mime])) {
throw new RuntimeException('不正なファイルタイプです: ' . $mime);
}
$filename = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
move_uploaded_file($_FILES['userfile']['tmp_name'], __DIR__ . '/uploads/' . $filename);
■ 拡張子やファイル名をそのまま使うリスク
- 重複ファイル名:同名ファイルが上書きされる危険
- パスインジェクション:
../../../
のような経路で意図しない場所に書き込み - XSS対策:ユーザ入力を HTML に出すときは必ず
htmlspecialchars()
🧠 試験対策まとめ(ここが出る!)
チェック対象 | 信用できる? | 備考 |
---|---|---|
$_FILES['name'] | ❌ | 元のファイル名。重複・改竄の恐れあり |
$_FILES['type'] | ❌ | ブラウザ送信値。偽装可能 |
$_FILES['tmp_name'] | ✅ | 実際の一時保存ファイルパス |
finfo_file() | ✅ | サーバ側で判定する正しい方法 |
move_uploaded_file() | ✅ | 正しいアップロードのみ許可 |
■ 図解まとめ:「正しい判定フロー」
▼
ユーザがアップロード
│
▼
PHPが$_FILESを受け取る
│
▼
[誤] $_FILES['type']で判断 → ❌偽装される
│
▼
[正] finfo_file() でMIME判定 → ✅サーバ側で安全確認
│
▼
move_uploaded_file()で保存
🏁 まとめ
PHPでファイルアップロードを扱う際は、「ファイル名・拡張子・タイプ情報を絶対に信用しない」のが鉄則。
$_FILES['type']
はヒント程度に考え、サーバ側で finfo や getimagesize で再検証するのが安全です。