PHPでファイルをアップロードする方法について

採用はこちら

PHPでは、HTMLフォームから送信された画像やPDF、CSVなどのファイルを受け取り、サーバー上の指定した場所へ保存できます。

基本的な流れは、以下のとおりです。

  1. HTMLでファイル選択フォームを作成する
  2. multipart/form-data形式でファイルを送信する
  3. PHPの$_FILESでアップロード情報を取得する
  4. アップロードエラーを確認する
  5. ファイルサイズや種類を検証する
  6. 安全な保存ファイル名を生成する
  7. move_uploaded_file()でファイルを保存する

PHPでファイルをアップロードする処理自体は難しくありませんが、実際のWebサイトで使用する場合はセキュリティ対策が重要です。

単純に受信したファイルをそのまま保存するのではなく、ファイルサイズやMIMEタイプ、保存先、ファイル名などを十分に確認する必要があります。

目次

HTMLでファイルアップロードフォームを作成する

基本的なHTMLフォーム

PHPへファイルを送信するには、まずHTML側にファイル選択フォームを用意します。

<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="upload_file">
    <button type="submit">アップロード</button>
</form>

ファイルアップロードでは、特に次の設定が重要です。

method="post"
enctype="multipart/form-data"

method="post"を指定することで、ファイルをPOSTリクエストとして送信できます。

また、enctype="multipart/form-data"を指定することで、ファイルを含むフォームデータを送信できるようになります。

enctypeを指定し忘れると、PHP側で正しくファイルを受け取れません。

input要素のname属性も必要

ファイル選択欄には、次のようにname属性を付けます。

<input type="file" name="upload_file">

PHPでは、このname属性をキーとしてファイル情報を取得します。

例えば、

name="upload_file"

なら、PHP側では、

$_FILES['upload_file']

としてアクセスできます。

PHPの$_FILESでアップロードファイルを受け取る

$_FILESとは

ファイルアップロードでは、PHPのスーパーグローバル変数である$_FILESを使用します。

例えば、次のHTMLがある場合、

<input type="file" name="upload_file">

PHP側では、

$_FILES['upload_file']

にファイル情報が格納されます。

確認する場合は、次のように記述できます。

<?php

var_dump($_FILES['upload_file']);

$_FILESに格納される主な情報

$_FILES['upload_file']には、主に次の情報が含まれます。

項目内容
nameクライアント側の元ファイル名
typeクライアントから通知されたMIMEタイプ
tmp_namePHPが一時保存したファイルのパス
errorアップロード時のエラーコード
sizeファイルサイズ

例えば、次のように取得できます。

$fileName = $_FILES['upload_file']['name'];
$tmpName  = $_FILES['upload_file']['tmp_name'];
$error    = $_FILES['upload_file']['error'];
$size     = $_FILES['upload_file']['size'];

ただし、nametypeはユーザー側から送られてくる情報を含むため、そのまま安全な値として信用するのは避ける必要があります。

アップロードエラーを確認する

errorの確認が重要

ファイルを受け取ったら、最初に確認したいのが、

$_FILES['upload_file']['error']

です。

正常にアップロードされた場合は、

UPLOAD_ERR_OK

になります。

例えば、次のように確認できます。

<?php

if (!isset($_FILES['upload_file'])) {
    exit('ファイルが送信されていません。');
}

if ($_FILES['upload_file']['error'] !== UPLOAD_ERR_OK) {
    exit('アップロードに失敗しました。');
}

エラー確認をせずにファイル処理を進めると、不完全なファイルや存在しない一時ファイルを扱ってしまう可能性があります。

主なUPLOAD_ERR定数

PHPでは、アップロードエラーを定数で判定できます。

定数内容
UPLOAD_ERR_OK正常にアップロードされた
UPLOAD_ERR_INI_SIZEupload_max_filesizeを超えた
UPLOAD_ERR_FORM_SIZEHTMLのMAX_FILE_SIZEを超えた
UPLOAD_ERR_PARTIALファイルの一部しか送信されなかった
UPLOAD_ERR_NO_FILEファイルが選択されていない
UPLOAD_ERR_NO_TMP_DIR一時ディレクトリが存在しない
UPLOAD_ERR_CANT_WRITEディスクへの書き込みに失敗した
UPLOAD_ERR_EXTENSIONPHP拡張機能によって停止された

ファイルが選択されていない場合も、ファイル名などから判断するのではなく、エラーコードを利用して判定するのが基本です。

move_uploaded_file()でファイルを保存する

基本的な使い方

PHPでアップロードされた一時ファイルを指定した保存先へ移動するときは、

move_uploaded_file()

を使用します。

例えば、単純な例では次のようになります。

<?php

if (
    isset($_FILES['upload_file']) &&
    $_FILES['upload_file']['error'] === UPLOAD_ERR_OK
) {
    $tmpFile = $_FILES['upload_file']['tmp_name'];

    $destination = __DIR__ . '/uploads/sample.jpg';

    if (move_uploaded_file($tmpFile, $destination)) {
        echo 'アップロードに成功しました。';
    } else {
        echo 'ファイルの保存に失敗しました。';
    }
}

move_uploaded_file()は、次の形式で使用します。

move_uploaded_file(
    $tmpFile,
    $destination
);

第1引数に一時ファイル、第2引数に保存先のパスを指定します。

move_uploaded_file()を使う理由

move_uploaded_file()は、指定された元ファイルがPHPのHTTP POSTアップロードによって作成された有効なファイルかどうかを確認したうえで移動します。

そのため、通常のファイルアップロード処理では、単純なrename()などよりもmove_uploaded_file()を利用するのが基本です。

成功した場合はtrue、失敗した場合はfalseを返します。

ファイルサイズを確認する

PHP側でも容量制限を行う

アップロード可能なファイルサイズは、アプリケーション側でも確認する必要があります。

例えば、5MBまで許可する場合は次のようにします。

$maxSize = 5 * 1024 * 1024;

if ($_FILES['upload_file']['size'] > $maxSize) {
    exit('ファイルサイズは5MB以下にしてください。');
}

5MBは、

5 × 1024 × 1024

なので、5,242,880バイトです。

php.iniにも容量制限がある

PHP自体にもファイルアップロード容量に関係する設定があります。

代表的なのが、

upload_max_filesize
post_max_size

です。

例えば、

upload_max_filesize = 10M
post_max_size = 12M

のように設定します。

upload_max_filesizeは、1ファイルあたりの最大アップロードサイズに関係します。

一方、post_max_sizeはPOSTリクエスト全体の最大サイズを制御します。

そのため、通常は、

post_max_size > upload_max_filesize

となるように設定します。

post_max_sizeを超えると$_FILESが空になることがある

通常のエラー処理では検出できないケース

特に注意したいのが、POSTデータ全体がpost_max_sizeを超えた場合です。

この場合、PHPでは$_POST$_FILESが空になることがあります。

そのため、

$_FILES['upload_file']['error']

だけでエラーを確認しようとしても、そもそも$_FILES['upload_file']が存在しない場合があります。

実運用では必要に応じて、

$_SERVER['CONTENT_LENGTH']

なども利用し、POSTサイズ超過を検出する設計を検討します。

MIMEタイプを確認する

$_FILES[‘type’]だけを信用しない

PHPでは、

$_FILES['upload_file']['type']

からMIMEタイプらしき情報を取得できます。

しかし、この値はクライアント側から通知される情報なので、ファイルの安全性確認には十分ではありません。

そのため、サーバー側でファイル内容を調べる方法としてFileinfoを使用するのが一般的です。

finfoでMIMEタイプを調べる

例えば、次のように記述できます。

$finfo = new finfo(FILEINFO_MIME_TYPE);

$mimeType = $finfo->file(
    $_FILES['upload_file']['tmp_name']
);

取得したMIMEタイプを、許可する種類の一覧と照合します。

例えば、画像だけを許可するなら次のようにします。

$allowedTypes = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
];

if (!isset($allowedTypes[$mimeType])) {
    exit('許可されていないファイル形式です。');
}

このように、許可する形式だけを明示する方式を採用すると安全性を高められます。

MIMEタイプだけで安全性を判断しない

複数のチェックを組み合わせる

FileinfoによるMIMEタイプ判定は有効ですが、MIMEタイプを確認しただけでファイルの安全性が完全に保証されるわけではありません。

例えば画像アップロードであれば、

  • ファイルサイズ
  • MIMEタイプ
  • 画像として正常に読み込めるか
  • 画像サイズ
  • 保存先
  • 保存ファイル名
  • Webサーバーの実行設定

などを組み合わせて確認するのが望ましいです。

特にユーザーがアップロードしたファイルをWeb公開ディレクトリに保存する場合、PHPなどのスクリプトとして実行されないようサーバー側を設定することが重要です。

拡張子だけでファイル形式を判断しない

.jpgでも画像とは限らない

例えば、

sample.jpg

というファイル名だからといって、中身が必ずJPEG画像とは限りません。

そのため、

pathinfo(
    $_FILES['upload_file']['name'],
    PATHINFO_EXTENSION
);

だけを利用してファイル形式を判断する方法は不十分です。

拡張子は補助情報として扱い、実際のファイル内容についてはサーバー側で確認することが重要です。

元のファイル名をそのまま保存しない

ユーザー指定の名前には注意する

アップロードされた元ファイル名は、

$_FILES['upload_file']['name']

から取得できます。

しかし、次のようにそのまま保存名へ使用する方法は避けたほうがよいでしょう。

$destination =
    __DIR__ .
    '/uploads/' .
    $_FILES['upload_file']['name'];

元ファイル名をそのまま使うと、

  • 同名ファイルの衝突
  • 特殊文字
  • 不適切なファイル名
  • パスに関する問題
  • ファイル名の推測

などの問題につながる可能性があります。

ランダムな保存ファイル名を生成する

random_bytes()を利用する

保存名は、サーバー側で新しく生成する方法が適しています。

例えば、

$fileName = bin2hex(random_bytes(16));

とすると、ランダムな文字列を生成できます。

MIMEタイプに対応した拡張子を付けるなら、次のようにします。

$allowedTypes = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
];

$extension = $allowedTypes[$mimeType];

$fileName =
    bin2hex(random_bytes(16)) .
    '.' .
    $extension;

例えば、

7ce121a56fec5ad37883d8c4880f44fa.jpg

のような保存名になります。

ユーザー側のファイル名ではなく、サーバー側で安全な名前を生成できるのがメリットです。

同名ファイルの上書きに注意する

move_uploaded_file()は保存先を上書きする場合がある

保存先に同じ名前のファイルが存在する場合、move_uploaded_file()によって既存ファイルが上書きされる可能性があります。

そのため、元ファイル名をそのまま使用するよりも、

bin2hex(random_bytes(16))

などを利用して、一意性の高い保存名を生成する方法が適しています。

アップロード先ディレクトリを用意する

基本的なディレクトリ構成

例えば、

$uploadDir = __DIR__ . '/uploads/';

とする場合、次のような構成になります。

project/
├── upload.php
└── uploads/

PHPを実行するWebサーバーやPHP-FPMの実行ユーザーが、uploadsディレクトリへ書き込める必要があります。

書き込み可能か確認する

実用的なコードでは、次のような確認も行うと安全です。

if (!is_dir($uploadDir)) {
    exit('アップロード先ディレクトリが存在しません。');
}

if (!is_writable($uploadDir)) {
    exit('アップロード先ディレクトリに書き込めません。');
}

これにより、保存処理が失敗した原因を把握しやすくなります。

ディレクトリ権限は必要最小限にする

無条件に777へ変更しない

アップロード先に書き込み権限が必要だからといって、

chmod 777 uploads

のように無条件で最大限の権限を与えるのは推奨されません。

サーバー環境に応じて、

  • Webサーバーの実行ユーザー
  • PHP-FPMの実行ユーザー
  • ファイル所有者
  • グループ
  • ディレクトリ権限

を確認し、必要最小限の権限に設定するのが基本です。

accept属性だけではファイル形式を制限できない

acceptはユーザー操作を補助する属性

HTMLでは、例えば次のように指定できます。

<input
    type="file"
    name="upload_file"
    accept="image/jpeg,image/png,image/gif,image/webp"
>

accept属性を指定すると、ファイル選択画面で対象となるファイルを絞り込みやすくなります。

ただし、acceptはあくまでブラウザ側の補助機能です。

セキュリティ上のファイル制限として信用してはいけません。

サーバー側でも必ず、

$allowedTypes

などを利用してファイル形式を検証する必要があります。

MAX_FILE_SIZEだけでは容量制限にならない

クライアント側の値は変更できる

HTMLでは、次のように記述できます。

<input
    type="hidden"
    name="MAX_FILE_SIZE"
    value="5242880"
>

この値を超えると、PHP側でUPLOAD_ERR_FORM_SIZEになる場合があります。

ただし、HTML側の値はユーザーが変更できるため、セキュリティ上の容量制限として信用することはできません。

必ずPHP側でも、

$maxSize = 5 * 1024 * 1024;

if ($file['size'] > $maxSize) {
    exit('ファイルサイズは5MB以下にしてください。');
}

のように確認します。

実用的なPHPファイルアップロードコード

安全性を考慮した基本例

ここまでの内容をまとめると、画像アップロードは次のように実装できます。

<?php

$uploadDir = __DIR__ . '/uploads/';

$maxSize = 5 * 1024 * 1024;

$allowedTypes = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
];

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    exit('不正なリクエストです。');
}

if (!is_dir($uploadDir)) {
    exit('アップロード先ディレクトリが存在しません。');
}

if (!is_writable($uploadDir)) {
    exit('アップロード先ディレクトリに書き込めません。');
}

if (!isset($_FILES['upload_file'])) {
    exit('ファイルが送信されていません。');
}

$file = $_FILES['upload_file'];

if ($file['error'] !== UPLOAD_ERR_OK) {
    exit('アップロードエラーが発生しました。');
}

if ($file['size'] <= 0) {
    exit('空のファイルはアップロードできません。');
}

if ($file['size'] > $maxSize) {
    exit('ファイルサイズは5MB以下にしてください。');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);

$mimeType = $finfo->file($file['tmp_name']);

if ($mimeType === false) {
    exit('ファイル形式を判定できませんでした。');
}

if (!isset($allowedTypes[$mimeType])) {
    exit('許可されていないファイル形式です。');
}

$extension = $allowedTypes[$mimeType];

$fileName =
    bin2hex(random_bytes(16)) .
    '.' .
    $extension;

$destination =
    $uploadDir .
    $fileName;

if (!move_uploaded_file(
    $file['tmp_name'],
    $destination
)) {
    exit('ファイルの保存に失敗しました。');
}

echo 'アップロードに成功しました。';

HTML側は、次のようにできます。

<form
    action="upload.php"
    method="post"
    enctype="multipart/form-data"
>
    <input
        type="file"
        name="upload_file"
        accept="image/jpeg,image/png,image/gif,image/webp"
        required
    >

    <button type="submit">
        アップロード
    </button>
</form>

このように、

ファイルを受信
↓
アップロードエラーを確認
↓
ファイルサイズを確認
↓
MIMEタイプを確認
↓
許可する形式か確認
↓
安全な保存名を生成
↓
move_uploaded_file()で保存

という順番で処理すると、実装の流れを整理しやすくなります。

複数ファイルをアップロードする方法

HTMLではmultipleを指定する

複数ファイルを選択できるようにする場合は、次のように記述します。

<input type="file" name="files[]" multiple>

PHP側では、$_FILES内の情報が配列として格納されます。

例えば、

foreach ($_FILES['files']['error'] as $index => $error) {
    if ($error !== UPLOAD_ERR_OK) {
        continue;
    }

    $tmpName =
        $_FILES['files']['tmp_name'][$index];

    // ファイルサイズを確認
    // MIMEタイプを確認
    // 保存ファイル名を生成
    // move_uploaded_file()で保存
}

のように1ファイルずつ処理します。

複数ファイルの場合でも、それぞれについて安全性を検証する必要があります。

max_file_uploadsにも注意する

1回に処理できるファイル数には上限がある

PHPでは、

max_file_uploads

によって、1リクエストでアップロードできるファイル数に上限を設定できます。

複数ファイルを扱う場合は、

max_file_uploads = 20

などの設定も確認します。

大量のファイルをアップロードする仕組みを作る場合は、ファイル容量だけでなくファイル数の制限も考慮する必要があります。

max_input_timeにも注意する

大容量ファイルや低速回線では時間制限が関係する

大きなファイルをアップロードする場合や通信速度が遅い場合、

max_input_time

が影響する可能性があります。

max_input_timeは、PHPが入力データを受け取るために利用できる時間に関係する設定です。

大容量ファイルを扱うシステムでは、

  • upload_max_filesize
  • post_max_size
  • max_input_time

などを総合的に確認する必要があります。

また、ApacheやNginx、PHP-FPM、リバースプロキシ、CDNなどにも個別の容量制限やタイムアウト設定が存在する場合があります。

PHP側の設定だけを変更しても解決しない場合は、Webサーバー側の設定も確認しましょう。

php.iniの設定値を確認する

ini_get()を利用する

現在のアップロード設定は、ini_get()で確認できます。

<?php

echo ini_get('upload_max_filesize');
echo '<br>';
echo ini_get('post_max_size');

例えば、

10M
12M

のように表示されます。

注意したいのは、ini_get()が返す容量値は、

2M
10M
128M

といった文字列であることです。

そのままバイト数として計算しないように注意しましょう。

ini_parse_quantity()で容量をバイト数へ変換できる

PHPの容量表記を数値化する

対応するPHP環境では、

ini_parse_quantity()

を使用すると、

$bytes = ini_parse_quantity(
    ini_get('upload_max_filesize')
);

のようにして容量表記を数値へ変換できます。

例えば、

10M

のような値をバイト単位で扱いたい場合に便利です。

ini_set()でアップロード上限を変えられない場合がある

アップロード後に変更しても遅い

例えば、スクリプト内で、

ini_set('upload_max_filesize', '100M');

と記述しても、そのリクエストのファイルアップロード上限を変更する用途には基本的に使えません。

ファイルの受信処理はPHPスクリプト本体が実行される前に行われるためです。

アップロード上限を変更する場合は、環境に応じて、

php.ini
.user.ini
PHP-FPM設定
Webサーバー設定

などを変更します。

is_uploaded_file()は必須なのか

move_uploaded_file()でもアップロード元を確認する

PHPには、

is_uploaded_file()

という関数があります。

これは、指定したファイルがPHPのHTTP POSTアップロードによって作成されたファイルか確認するための関数です。

一方、

move_uploaded_file()

自体も、移動元が有効なアップロードファイルであることを確認します。

そのため、受信した一時ファイルをそのままmove_uploaded_file()で保存するだけなら、同じ確認目的でis_uploaded_file()を必ず追加する必要はありません。

ただし、move_uploaded_file()で保存する前に一時ファイルへ別の処理を行う場合は、is_uploaded_file()による確認を検討できます。

PHP 8.4以降ではrequest_parse_body()も利用できる

POST以外のmultipart/form-dataを扱える

通常のHTMLフォームによるPOSTアップロードでは、$_FILESを利用すれば十分です。

一方、PHP 8.4以降では、

request_parse_body()

という関数も利用できます。

例えば、

[$post, $files] = request_parse_body();

のようにリクエストボディを解析できます。

特に、PUTPATCHなど、POST以外のHTTPメソッドでmultipart/form-dataを扱いたい場合に利用できます。

一般的なHTMLフォームからのPOSTアップロードで、無理に使用する必要はありません。

PHPのファイルアップロードでよくある失敗

$_FILESが空になる

$_FILESが空の場合は、次の項目を確認します。

formのmethodがpostになっているか
enctype="multipart/form-data"を指定しているか
inputにname属性があるか
post_max_sizeを超えていないか

特にpost_max_sizeを超えた場合は、$_POST$_FILES自体が空になる可能性があります。

move_uploaded_file()が失敗する

move_uploaded_file()が失敗する場合は、

  • 保存先ディレクトリが存在するか
  • 保存先に書き込み権限があるか
  • 保存先パスが正しいか
  • 一時ファイルが有効か
  • PHPやWebサーバーの制限に引っかかっていないか

などを確認します。

大きなファイルだけアップロードできない

小さなファイルは送信できるのに、大きなファイルだけ失敗する場合は、

upload_max_filesize
post_max_size
max_input_time

などを確認します。

Webサーバーやリバースプロキシ側にも別の制限が設定されている可能性があります。

ファイルアップロードのセキュリティ対策

許可するファイル形式を限定する

アップロードできる形式は、必要なものだけに限定します。

例えば画像だけなら、

$allowedTypes = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/webp' => 'webp',
];

のようにホワイトリスト方式で管理するのが基本です。

元ファイル名を信用しない

元ファイル名をそのまま保存せず、サーバー側で安全な名前を生成します。

ファイル容量を制限する

アプリケーション側とPHP設定側の両方で制限します。

アップロード先でスクリプトを実行させない

ユーザーがアップロードしたファイルをWebから直接参照できる場所に保存する場合は、PHPなどとして実行されない設定にします。

可能であれば、Web公開ディレクトリの外側に保存する方法も検討できます。

ファイルの内容も検証する

画像であれば、単にMIMEタイプだけを見るのではなく、必要に応じて画像として正常に読み込めるかなども確認します。

PHPでファイルをアップロードする際のポイント

PHPでファイルアップロードを実装する場合、最低限のコードはそれほど複雑ではありません。

HTMLでは、

<form
    action="upload.php"
    method="post"
    enctype="multipart/form-data"
>
    <input type="file" name="upload_file">
    <button type="submit">アップロード</button>
</form>

PHPでは、

move_uploaded_file(
    $_FILES['upload_file']['tmp_name'],
    $destination
);

という処理が基本になります。

ただし、実際のWebサービスでは、単にアップロードできればよいわけではありません。

重要なのは、

$_FILESで受信
↓
UPLOAD_ERRでエラー確認
↓
ファイルサイズを確認
↓
MIMEタイプを確認
↓
許可するファイル形式を限定
↓
サーバー側で保存名を生成
↓
安全な保存先を指定
↓
move_uploaded_file()で保存

という順番で、安全性を確認しながら処理することです。

PHPのファイルアップロードでは、元ファイル名や拡張子、クライアントから送信されたMIMEタイプだけを信用せず、サーバー側で検証してから保存することが最も重要です。

以上、PHPでファイルをアップロードする方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次