Saltar al contenido

Cómo implementar la carga de archivos en PHP | Guía completa de validación, almacenamiento y seguridad

Categoría: PHP / Implementación
Este artículo está disponible actualmente solo en japonés. Estamos trabajando en las traducciones.

Al implementar la funcionalidad de carga de archivos en PHP, escribir código "que funcione" es fácil, pero crear implementaciones robustas que consideren seguridad, validación y manejo de errores es sorprendentemente complejo. Este artículo explica sistemáticamente métodos de implementación adecuados para entornos de producción, comenzando desde la estructura de <code>$_FILES</code>, pasando por validación de tipos MIME, listas blancas de extensiones, rutas de almacenamiento seguras y desinfección de nombres de archivo.

Browser PHP-FPM /tmp uploads/ POST multipart/form-data tmp file write $_FILES populated move_uploaded_file()
Diagrama: secuencia de procesamiento de $_FILES (POST → /tmp → move_uploaded_file)

Comprender la estructura de <code>$_FILES</code>

Cuando envías un archivo en un formulario HTML con <code>enctype="multipart/form-data"</code>, PHP almacena la información de carga en la variable superglobal <code>$_FILES</code>. Si el atributo <code>name</code> del formulario es <code>userfile</code>, <code>$_FILES['userfile']</code> se convierte en un arreglo asociativo con las siguientes 5 claves.

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

Como punto importante, <code>type</code> es un valor enviado por el cliente y puede ser falsificado. Siempre use <code>finfo</code> en el lado del servidor para verificar el tipo MIME.

Lista de códigos de error UPLOAD_ERR_* y soluciones

<code>$_FILES['userfile']['error']</code> contiene uno de los siguientes valores constantes. Es importante verificar este valor al principio del proceso de carga y manejar los errores apropiadamente.

Constante Valor Significado Medida correctiva
UPLOAD_ERR_OK 0 Carga exitosa Continuar procesamiento
UPLOAD_ERR_INI_SIZE 1 Se excedió upload_max_filesize en php.ini Revisar la configuración de php.ini
UPLOAD_ERR_FORM_SIZE 2 Excede MAX_FILE_SIZE del formulario HTML Revisar la configuración del formulario
UPLOAD_ERR_PARTIAL 3 La transferencia de archivo se completó solo parcialmente Solicitar re-carga
UPLOAD_ERR_NO_FILE 4 Ningún archivo seleccionado Solicitar selección de archivo
UPLOAD_ERR_NO_TMP_DIR 6 El directorio temporal no existe Verificar la configuración del servidor
UPLOAD_ERR_CANT_WRITE 7 Error de escritura en disco Verificar espacio en disco y permisos
UPLOAD_ERR_EXTENSION 8 Carga detenida por extensión de PHP Verificar la Configuración del Módulo de Extensión
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               => '不明なエラーが発生しました。',
    };
}

Validación de tipo MIME (usando finfo)

<code>$_FILES['userfile']['type']</code> enviado por el cliente puede ser falsificado, por lo que el servidor debe determinar el tipo MIME del contenido del archivo real. Use la función <code>finfo_file()</code> de PHP (o la clase <code>FileInfo</code>).

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> determina el tipo MIME leyendo los primeros bytes (número mágico) de un archivo, por lo que puede detectar ataques con extensiones falsificadas. Sin embargo, los archivos de texto (CSV, JSON, etc.) pueden detectarse como <code>text/plain</code>, por lo que se recomienda combinarlo con la verificación de extensión.

Lista Blanca de Extensiones

Además de la validación del tipo MIME, también realice una verificación de lista blanca en las extensiones de archivo. Una lista blanca (listando solo lo que está permitido) es más segura que una lista negra (prohibiendo .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('ファイル形式と拡張子が一致しません。');
}

Ejemplo de código para límites de tamaño de archivo

Las comprobaciones de tamaño a nivel de PHP se realizan usando <code>$_FILES['userfile']['size']</code>. Sin embargo, los archivos que exceden el límite <code>upload_max_filesize</code> en php.ini nunca llegan a <code>$_FILES</code> y generan un error <code>UPLOAD_ERR_INI_SIZE</code> en su lugar. Por lo tanto, es importante verificar el código de error primero.

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;
}

Ubicación de almacenamiento seguro (fuera de public)

Los archivos cargados deben almacenarse fuera de la raíz de documentos (como <code>public_html</code> o <code>public</code>). Guardarlos en el directorio <code>public</code> conlleva riesgos de acceso directo mediante URL y ejecución como archivos PHP.

// 推奨ディレクトリ構成
// /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)

Para aceptar cargas de archivos en PHP, debe configurar adecuadamente php.ini (o configuración local de .htaccess / php.ini). Es importante cambiar no solo <code>upload_max_filesize</code> sino también <code>post_max_size</code> juntos.

; 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

Puede verificar la aplicación de los ajustes usando <code>phpinfo()</code> o <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,
    };
}

Resumen: Lista de verificación para el procesamiento seguro de carga de archivos

  • Verificación de códigos de error con UPLOAD_ERR_*
  • El tipo MIME se determina en el lado del servidor usando <code>finfo</code>
  • Validar extensiones de archivo usando un enfoque de lista blanca
  • verificando el tamaño del archivo con <code>$_FILES['userfile']['size']</code>
  • El destino de guardado está configurado fuera del directorio public
  • Verificando cargas legítimas con <code>is_uploaded_file()</code>
  • Moviendo archivos con <code>move_uploaded_file()</code>
  • Generando nombres de archivo con <code>uniqid()</code> o <code>random_bytes()</code>
  • Configurar correctamente <code>upload_max_filesize</code> y <code>post_max_size</code> en php.ini

Archivos de prueba para este artículo

  • <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de archivos de prueba de valores límite</a> — Ideal para validar valores alrededor de upload_max_filesize
  • <a href="/ja/files/threshold/10mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Conjunto de prueba de valores límite de 10MB</a> — Conjunto de tres archivos: exacto, justo antes y justo después
  • <a href="/ja/files/broken/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de archivos corruptos e inválidos</a> — Pruebe la validación con archivos de suplantación de tipo MIME y extensión
  • <a href="/ja/files/images/png/" class="text-primary-600 dark:text-primary-400 hover:underline">Imágenes de prueba PNG</a> — Para validación de tipo MIME en cargas de imágenes

Artículos relacionados

  • <a href="/ja/blog/how-to-test-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Cómo probar correctamente los límites de tamaño de carga de archivos</a>
  • <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de verificación de implementación de validación de archivos en formularios web</a>
  • <a href="/ja/blog/multipart-form-data-overhead/" class="text-primary-600 dark:text-primary-400 hover:underline">Calcular la sobrecarga de multipart/form-data con precisión</a>
  • <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Resumen de los límites de carga de archivos para AWS S3 y CloudFront</a>

Preguntas frecuentes

¿Cuál es el valor predeterminado del límite de tamaño de archivo de carga de PHP?

El upload_max_filesize predeterminado en php.ini es 2MB. post_max_size también está configurado a 8MB.

¿Cómo cargar múltiples archivos simultáneamente en PHP?

Agregue el atributo <code>multiple</code> a la etiqueta <code>input</code> y establezca el atributo <code>name</code> en formato de matriz (<code>files[]</code>). Puede recibirlo como una matriz con <code>$_FILES['files']</code>.

¿Qué consideraciones de seguridad debo tener en cuenta para las cargas de archivos en PHP?

Es importante validar las extensiones de archivo con una lista blanca, verificar los tipos MIME usando <code>finfo_file()</code> y almacenar las cargas fuera de la raíz del documento.