Skip to content

How to implement file upload in PHP | Complete guide to validation, storage, and security

Category: PHP / Implementation
This article is currently available in Japanese only. We are working on translations.

When implementing file upload functionality in PHP, writing "just working" code is easy, but creating robust implementations that consider security, validation, and error handling is surprisingly deep. This article systematically explains implementation methods suitable for production environments, starting from the structure of <code>$_FILES</code>, through MIME type validation, extension whitelisting, safe storage paths, and filename sanitization.

Browser PHP-FPM /tmp uploads/ POST multipart/form-data tmp file write $_FILES populated move_uploaded_file()
Diagram: $_FILES processing sequence (POST → /tmp → move_uploaded_file)

Understanding the structure of <code>$_FILES</code>

When you submit a file in an HTML form with <code>enctype="multipart/form-data"</code>, PHP stores the upload information in the <code>$_FILES</code> superglobal variable. If the form's <code>name</code> attribute is <code>userfile</code>, <code>$_FILES['userfile']</code> becomes an associative array with the following 5 keys.

// $_FILES の構造
[
    'name'     => 'photo.jpg',        // クライアント側のファイル名
    'type'     => 'image/jpeg',       // クライアントが申告するMIMEタイプ
    'tmp_name' => '/tmp/phpA1B2C3',   // サーバーの一時保存パス
    'error'    => 0,                  // エラーコード(0 = 成功)
    'size'     => 204800,             // ファイルサイズ(バイト)
]

As an important point, <code>type</code> is a value sent by the client and can be forged. Always use <code>finfo</code> on the server side to verify the MIME type.

UPLOAD_ERR_* error codes and troubleshooting

<code>$_FILES['userfile']['error']</code> contains one of the following constant values. It is important to check this value at the beginning of the upload process and handle errors appropriately.

Constant Value Meaning Countermeasure
UPLOAD_ERR_OK 0 Upload successful Continue Processing
UPLOAD_ERR_INI_SIZE 1 Exceeded upload_max_filesize in php.ini Review php.ini configuration
UPLOAD_ERR_FORM_SIZE 2 Exceeds HTML form MAX_FILE_SIZE Review form settings
UPLOAD_ERR_PARTIAL 3 File transfer was only partially completed Prompt for re-upload
UPLOAD_ERR_NO_FILE 4 No file selected Prompt file selection
UPLOAD_ERR_NO_TMP_DIR 6 Temporary directory does not exist Check server settings
UPLOAD_ERR_CANT_WRITE 7 Disk write failure Check disk space and permissions
UPLOAD_ERR_EXTENSION 8 Upload stopped by PHP extension Verify Extension Module Configuration
function getUploadErrorMessage(int $errorCode): string
{
    return match ($errorCode) {
        UPLOAD_ERR_INI_SIZE   => 'ファイルサイズがサーバーの上限を超えています。',
        UPLOAD_ERR_FORM_SIZE  => 'ファイルサイズがフォームの上限を超えています。',
        UPLOAD_ERR_PARTIAL    => 'ファイルが完全にアップロードされませんでした。再度お試しください。',
        UPLOAD_ERR_NO_FILE    => 'ファイルが選択されていません。',
        UPLOAD_ERR_NO_TMP_DIR => 'サーバーエラー: 一時ディレクトリが見つかりません。',
        UPLOAD_ERR_CANT_WRITE => 'サーバーエラー: ファイルの書き込みに失敗しました。',
        UPLOAD_ERR_EXTENSION  => 'サーバーエラー: 拡張機能によりアップロードが拒否されました。',
        default               => '不明なエラーが発生しました。',
    };
}

MIME Type Validation (Using finfo)

The <code>$_FILES['userfile']['type']</code> sent by the client can be forged, so the server must determine the MIME type from the actual file content. Use PHP's <code>finfo_file()</code> (or the <code>FileInfo</code> class).

function validateMimeType(string $tmpPath, array $allowedMimes): bool
{
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $detectedMime = $finfo->file($tmpPath);

    return in_array($detectedMime, $allowedMimes, true);
}

// 使用例: 画像ファイルのみ許可
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validateMimeType($_FILES['userfile']['tmp_name'], $allowedMimes)) {
    throw new RuntimeException('許可されていないファイル形式です。');
}

<code>finfo</code> determines MIME type by reading the first bytes (magic number) of a file, so it can detect attacks with forged extensions. However, text files (CSV, JSON, etc.) may be detected as <code>text/plain</code>, so combining it with extension checking is recommended.

Extension Whitelist

In addition to MIME type validation, also perform a whitelist check on file extensions. A whitelist (listing only what is allowed) is safer than a blacklist (prohibiting .php, .exe, etc.).

function validateExtension(string $originalName, array $allowedExtensions): bool
{
    // pathinfo() で拡張子を取得し、小文字に正規化
    $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

    return in_array($extension, $allowedExtensions, true);
}

// 使用例
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!validateExtension($_FILES['userfile']['name'], $allowedExtensions)) {
    throw new RuntimeException('許可されていない拡張子です。');
}

// MIMEタイプと拡張子の対応を明示的にマッピングする方法
$mimeToExtensions = [
    'image/jpeg' => ['jpg', 'jpeg'],
    'image/png'  => ['png'],
    'image/gif'  => ['gif'],
    'image/webp' => ['webp'],
];

$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($_FILES['userfile']['tmp_name']);
$extension = strtolower(pathinfo($_FILES['userfile']['name'], PATHINFO_EXTENSION));

if (
    !isset($mimeToExtensions[$detectedMime]) ||
    !in_array($extension, $mimeToExtensions[$detectedMime], true)
) {
    throw new RuntimeException('ファイル形式と拡張子が一致しません。');
}

Code Example for File Size Limits

At the PHP level, size checks are performed using <code>$_FILES['userfile']['size']</code>. However, files exceeding the <code>upload_max_filesize</code> limit in php.ini never reach <code>$_FILES</code> and result in an <code>UPLOAD_ERR_INI_SIZE</code> error instead. Therefore, it is important to check the error code first.

const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MiB

function validateFileSize(int $fileSize, int $maxSize = MAX_UPLOAD_SIZE): bool
{
    return $fileSize > 0 && $fileSize <= $maxSize;
}

// 使用例
if (!validateFileSize($_FILES['userfile']['size'])) {
    $maxMiB = MAX_UPLOAD_SIZE / (1024 * 1024);
    throw new RuntimeException("ファイルサイズが上限({$maxMiB} MiB)を超えています。");
}

// アップロード処理を関数にまとめた例
function processUpload(array $file): array
{
    // 1. エラーコード確認
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException(getUploadErrorMessage($file['error']));
    }

    // 2. サイズチェック
    if (!validateFileSize($file['size'])) {
        throw new RuntimeException('ファイルサイズが上限を超えています。');
    }

    // 3. MIMEタイプチェック
    $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!validateMimeType($file['tmp_name'], $allowedMimes)) {
        throw new RuntimeException('許可されていないファイル形式です。');
    }

    // 4. 拡張子チェック
    $allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    if (!validateExtension($file['name'], $allowedExts)) {
        throw new RuntimeException('許可されていない拡張子です。');
    }

    return $file;
}

Secure storage location (outside public)

Uploaded files should be stored outside the document root (such as <code>public_html</code> or <code>public</code>). Storing them inside the <code>public</code> directory risks direct URL access and execution as PHP files.

// 推奨ディレクトリ構成
// /var/www/
//   ├── public/         ← ドキュメントルート(外部からアクセス可能)
//   │   └── index.php
//   └── storage/        ← public の外(外部から直接アクセス不可)
//       └── uploads/

// 定数で保存先を明示
define('UPLOAD_DIR', dirname(__DIR__) . '/storage/uploads/');

// ディレクトリが存在しない場合は作成
if (!is_dir(UPLOAD_DIR)) {
    mkdir(UPLOAD_DIR, 0755, true);
}

// ダウンロード提供時は PHP 経由でストリーミング
function serveFile(string $filename): void
{
    $filePath = UPLOAD_DIR . basename($filename);

    if (!file_exists($filePath)) {
        http_response_code(404);
        exit;
    }

    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($filePath);

    header('Content-Type: ' . $mimeType);
    header('Content-Length: ' . filesize($filePath));
    header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
    readfile($filePath);
    exit;
}

ファイル名のサニタイズ(uniqid使用)

クライアントが送信したファイル名をそのまま使用するのは危険です。ディレクトリトラバーサル(../../etc/passwd のようなパス)や、特殊文字を含むファイル名によるOSコマンドインジェクションのリスクがあります。uniqid() を使ってユニークなファイル名を生成し、元のファイル名は別途データベースなどに記録する方法が安全です。

function generateSafeFilename(string $originalName): string
{
    // 拡張子のみ元のファイルから引き継ぐ
    $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

    // uniqid() + mt_rand() でユニークな名前を生成
    // more_entropy=true で精度を上げる
    $uniqueName = uniqid('upload_', true);

    // さらにランダム性を加える場合は bin2hex(random_bytes(8)) を使う
    // $uniqueName = bin2hex(random_bytes(16));

    return $uniqueName . '.' . $extension;
}

// 使用例
$safeFilename = generateSafeFilename($_FILES['userfile']['name']);
$savePath = UPLOAD_DIR . $safeFilename;

// is_uploaded_file() で正規のアップロードか確認してから移動
if (!is_uploaded_file($_FILES['userfile']['tmp_name'])) {
    throw new RuntimeException('不正なファイルアップロードを検出しました。');
}

if (!move_uploaded_file($_FILES['userfile']['tmp_name'], $savePath)) {
    throw new RuntimeException('ファイルの保存に失敗しました。');
}

// 元のファイル名、保存名、MIMEタイプをDBに記録
// db_insert(['original_name' => $_FILES['userfile']['name'], 'saved_name' => $safeFilename, ...]);

php.ini 設定(upload_max_filesize / post_max_size)

To accept file uploads in PHP, you must properly configure php.ini (or .htaccess / local php.ini settings). Importantly, you need to change not only <code>upload_max_filesize</code> but also <code>post_max_size</code> together.

; php.ini の設定
; ファイルアップロードを有効化
file_uploads = On

; 1ファイルあたりの上限(M = MiB 単位)
upload_max_filesize = 20M

; POSTリクエスト全体の上限
; upload_max_filesize より大きく設定する(フォームデータのオーバーヘッド分)
post_max_size = 25M

; 最大実行時間(大きなファイルのアップロードに対応)
max_execution_time = 300

; 最大入力時間(アップロードの読み込み時間)
max_input_time = 300

; メモリ上限(post_max_size より大きくする)
memory_limit = 128M

You can verify the application of settings using <code>phpinfo()</code> or <code>ini_get()</code>.

// 現在の設定値を確認
echo ini_get('upload_max_filesize');  // 例: "20M"
echo ini_get('post_max_size');        // 例: "25M"

// バイト単位に変換するユーティリティ
function convertToBytes(string $value): int
{
    $value = trim($value);
    $last = strtolower($value[-1]);
    $num = (int) $value;

    return match ($last) {
        'g' => $num * 1024 * 1024 * 1024,
        'm' => $num * 1024 * 1024,
        'k' => $num * 1024,
        default => $num,
    };
}

Summary: Checklist for secure file upload processing

  • Checking error codes with UPLOAD_ERR_*
  • MIME type is determined on the server side using <code>finfo</code>
  • Validating file extensions using a whitelist approach
  • checking the file size with <code>$_FILES['userfile']['size']</code>
  • The save destination is configured outside the public directory
  • Verifying legitimate uploads with <code>is_uploaded_file()</code>
  • Moving files with <code>move_uploaded_file()</code>
  • Generating file names with <code>uniqid()</code> or <code>random_bytes()</code>
  • Properly configured <code>upload_max_filesize</code> and <code>post_max_size</code> in php.ini

Test files for this article

  • <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Boundary Value Test File List</a> — Ideal for validating settings around upload_max_filesize
  • <a href="/ja/files/threshold/10mb/" class="text-primary-600 dark:text-primary-400 hover:underline">10MB Boundary Value Test Set</a> — Three-file set: exact, just before, and just after
  • <a href="/ja/files/broken/" class="text-primary-600 dark:text-primary-400 hover:underline">Corrupted and invalid files list</a> — Test validation with MIME type spoofing and extension spoofing files
  • <a href="/ja/files/images/png/" class="text-primary-600 dark:text-primary-400 hover:underline">PNG Test Images</a> — For MIME type validation of image uploads

Related articles

  • <a href="/ja/blog/how-to-test-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">How to Properly Test File Upload Size Limits</a>
  • <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Web Form File Validation Implementation Checklist</a>
  • <a href="/ja/blog/multipart-form-data-overhead/" class="text-primary-600 dark:text-primary-400 hover:underline">Calculate multipart/form-data overhead accurately</a>
  • <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Summary of file upload limits for AWS S3 and CloudFront</a>

Frequently Asked Questions

What is the default value of PHP's upload file size limit?

The default upload_max_filesize in php.ini is 2MB. post_max_size is also set to 8MB.

How to upload multiple files simultaneously in PHP?

Add the <code>multiple</code> attribute to the <code>input</code> tag and set the <code>name</code> attribute in array format (<code>files[]</code>). You can receive it as an array with <code>$_FILES['files']</code>.

What security considerations should I keep in mind for PHP file uploads?

It is important to validate file extensions against a whitelist, verify MIME types using <code>finfo_file()</code>, and store uploads outside the document root.