2026年9月10日
2026年9月10日
PHPでファイルアップロード処理を実装する方法|安全な検証・保存先・サイズ制限とWordPressでの書き方
PHPのファイルアップロードは、フォームから受け取ったファイルを $_FILES で確認し、move_uploaded_file() で保存するのが基本の流れです。ただし、そのまま保存するだけではPHPファイルを置かれて乗っ取られる危険があるため、「エラーコードの確認」「中身からのファイル種別の判定」「ファイル名の付け直し」「スクリプトが実行されない場所への保存」をセットで実装します。この記事では、フォームの書き方から安全な受け取り処理、サイズ制限の設定、うまくいかないときの確認手順、WordPressで実装する場合の関数までを説明します。
アップロードの仕組み
ブラウザからファイルを送ると、PHPはリクエストを受け取った時点でファイルを一時ディレクトリに保存し、その情報を $_FILES に入れます。スクリプトの処理が終わると一時ファイルは削除されるため、残したいファイルは処理中に move_uploaded_file() で移動させます。
$_FILES['フィールド名'] には次の値が入ります。
name… 利用者のPCでのファイル名(信用しない)type… ブラウザが申告したMIMEタイプ(信用しない)tmp_name… サーバー上の一時ファイルのパスerror… アップロード結果のコードsize… バイト数
name と type は送信側が自由に書き換えられるため、判定には使いません。
フォームを用意する
<form action="/upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
<input type="file" name="avatar" accept="image/jpeg,image/png,image/webp" required>
<button type="submit">アップロード</button>
</form>
enctype="multipart/form-data" と method="post" がないと、ファイルは送られません。accept 属性はファイル選択画面を絞るだけの補助で、サーバー側の検証の代わりにはなりません。他サイトから勝手に送信されるのを防ぐため、フォームにCSRFトークンを埋め込み、受け取り側で照合します。トークンは、フォームを表示する前に $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); のように生成しておきます。
安全な受け取り処理
次のコードは、画像(JPEG・PNG・WebP)を2MBまで受け付ける例です。各ステップの理由はコードの後で説明します。
<?php
session_start();
// 1. CSRF トークンの確認
$token = $_SESSION['csrf_token'] ?? '';
if ($token === '' || !hash_equals($token, (string) ($_POST['csrf_token'] ?? ''))) {
http_response_code(403);
exit('不正なリクエストです。');
}
$file = $_FILES['avatar'] ?? null;
// 2. エラーコードの確認
if (!$file || !is_int($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('アップロードに失敗しました。');
}
// 3. サイズの確認(2MB)
$maxBytes = 2 * 1024 * 1024;
if ($file['size'] > $maxBytes) {
http_response_code(400);
exit('ファイルが大きすぎます。');
}
// 4. 中身から MIME タイプを判定し、許可リストで照合
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!isset($allowed[$mime])) {
http_response_code(400);
exit('この形式のファイルはアップロードできません。');
}
// 5. ファイル名をサーバー側で付け直す
$newName = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
// 6. Web から直接実行されない場所へ保存
$dir = '/var/app/storage/uploads';
if (!move_uploaded_file($file['tmp_name'], $dir . '/' . $newName)) {
error_log('move_uploaded_file failed: ' . $newName);
http_response_code(500);
exit('保存に失敗しました。');
}
chmod($dir . '/' . $newName, 0644);
echo 'アップロードしました。';
エラーコードを最初に見る理由:error が UPLOAD_ERR_OK(0)以外のときは tmp_name が空のことがあり、後続の処理が意味を持ちません。is_int() の確認は、name="avatar[]" のように配列で送られてきた場合に処理が崩れるのを防ぐためです。
MIMEタイプを中身から判定する理由:$_FILES['type'] は申告値なので、PHPファイルに image/png と付けて送ることもできます。finfo はファイルの先頭のバイト列を見て種類を判定します。ただし、画像の中にスクリプトを紛れ込ませる手口もあるため、判定だけに頼らず、次の「保存先」の対策と組み合わせます。画像なら、GDやImagickで再エンコードして保存すると、画像以外のデータを落とせます。
ファイル名を付け直す理由:利用者のファイル名には ../ のようなディレクトリの移動や、shell.php.jpg のような二重拡張子、OSで扱えない文字が入ることがあります。ランダムな名前と、MIMEタイプから決めた拡張子にすれば、これらをまとめて避けられます。元のファイル名を表示に使いたい場合は、データベースに別に保存し、出力時に htmlspecialchars() でエスケープします。
保存先を公開ディレクトリの外にする理由:ドキュメントルートの外に保存すれば、万一スクリプトが置かれてもURLから実行されません。表示やダウンロードは、権限を確認するPHPスクリプトを通して返します。公開ディレクトリに置く必要がある場合は、そのディレクトリでPHPを実行しない設定(Apacheの .htaccess やNginxの location 設定)を入れます。
サイズ制限の設定
アップロードできるサイズは、PHPとWebサーバーの両方で決まります。
; php.ini(変更後は PHP-FPM / Web サーバーを再起動)
file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 20
upload_max_filesize… 1ファイルあたりの上限post_max_size… リクエスト全体の上限。フォームの他の項目も含むため、upload_max_filesizeより大きくしますmax_file_uploads… 1回に送れるファイル数
Nginxを使っている場合は client_max_body_size も上限になり、超えると「413 Request Entity Too Large」がPHPに届く前に返されます。
うまくいかないときの確認手順
症状ごとに、起きやすい順に確認します。
**$_FILES が空になる**
- フォームに
enctype="multipart/form-data"があるか - リクエスト全体が
post_max_sizeを超えていないか。超えると$_POSTも$_FILESも空になり、PHPのエラーログに「POST Content-Length ... exceeds the limit」のような警告が残ります - php.iniで
file_uploadsが有効か
**error が 0 以外になる**
| コード | 定数 | 主な原因 |
|---|---|---|
| 1 | UPLOAD_ERR_INI_SIZE |
upload_max_filesize を超えた |
| 2 | UPLOAD_ERR_FORM_SIZE |
フォームの MAX_FILE_SIZE を超えた |
| 3 | UPLOAD_ERR_PARTIAL |
通信が途中で切れた |
| 4 | UPLOAD_ERR_NO_FILE |
ファイルが選ばれていない |
| 6 | UPLOAD_ERR_NO_TMP_DIR |
一時ディレクトリがない |
| 7 | UPLOAD_ERR_CANT_WRITE |
ディスクに書き込めない(容量・権限) |
| 8 | UPLOAD_ERR_EXTENSION |
PHP拡張が処理を止めた |
**move_uploaded_file() が false を返す**保存先ディレクトリが存在するか、PHPを実行するユーザー(www-data など)に書き込み権限があるか、open_basedir の制限に保存先が含まれているかを確認します。権限の問題を 777 で解決するのは避け、ディレクトリの所有者を合わせます。
設定を変えたのに反映されないときは、phpinfo() で実際の値を確認します。CLIとWeb側で読み込むphp.iniが違うことがよくあります。確認後は phpinfo() のファイルを削除してください。
WordPressで実装する場合
WordPressのプラグインやテーマでアップロードを受け付けるなら、自前で move_uploaded_file() を書くより、コアの関数を使うほうが安全です。拡張子とMIMEタイプの照合(wp_check_filetype_and_ext())や、保存先の決定、ファイル名の重複回避を任せられます。
<?php
// フロント側のフォームから受け取る例(admin-post.php で処理)
add_action( 'admin_post_my_upload', function () {
check_admin_referer( 'my_upload_action', 'my_upload_nonce' );
if ( ! current_user_can( 'upload_files' ) ) {
wp_die( esc_html__( '権限がありません。' ), 403 );
}
// 管理画面以外で使うときは読み込みが必要
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_id = media_handle_upload( 'avatar', 0 );
if ( is_wp_error( $attachment_id ) ) {
wp_die( esc_html( $attachment_id->get_error_message() ) );
}
wp_safe_redirect( wp_get_referer() ?: home_url( '/' ) );
exit;
} );
フォーム側では wp_nonce_field( 'my_upload_action', 'my_upload_nonce' ) でnonceを出力し、action の値を my_upload にして admin_url( 'admin-post.php' ) へ送信します。media_handle_upload() はメディアライブラリに登録まで行い、wp_handle_upload() はファイルの保存だけを行います。許可するファイル形式は upload_mimes フィルターで調整できますが、SVGやHTMLなど中にスクリプトを含められる形式を安易に追加しないでください。
コードを入れる前にサイトのバックアップを取り、ステージング環境で動作を確認します。PHPの仕様やWordPressの関数の挙動は版によって変わることがあるため(2026年10月現在)、古い環境ではマニュアルの記述も確認してください。
まとめ
- フォームは
method="post"とenctype="multipart/form-data"、CSRFトークンを付ける $_FILESのnameとtypeは信用せず、errorの確認→サイズ→finfoによるMIME判定→ファイル名の付け直しの順に検証する- 保存先は公開ディレクトリの外か、PHPを実行しない設定のディレクトリにする
- サイズの上限は
upload_max_filesize・post_max_size、Nginxならclient_max_body_sizeも確認する - WordPressでは nonce と
upload_files権限を確認し、media_handle_upload()やwp_handle_upload()を使う