Situación actual: PHP2Shell. Dos vulnerabilidades críticas de PHP (CVSS 9.2) permiten ejecutar código malicioso sin autenticación. Parches desde el 21/10/2026.Qué hacer ahora →

Cómo procesa PHP las subidas de archivos

Entre el clic en «Subir» y su código hay en PHP toda una cadena de procesamiento. Esta página explica multipart/form-data, los archivos temporales, $_FILES, los límites y la relación con PHP2Shell.

Actualizado 6 min de lecturaConocimiento de PHP

En resumen: cuando un navegador sube un archivo, envía una petición POST en formato multipart/form-data, en la que los campos del formulario y el contenido de los archivos aparecen uno tras otro como partes individuales. PHP descompone esa petición por sí mismo, escribe cada parte de archivo en un archivo temporal dentro de upload_tmp_dir, anota los datos en $_FILES y solo entonces inicia su script. El script mueve el archivo con move_uploaded_file(); todo lo que no se mueve, PHP lo elimina al final de la petición.

El recorrido de un archivo desde el navegador hasta el script

Para quienes toman decisiones hay algo especialmente importante: una subida no es una operación que dependa solo de la aplicación. Buena parte del trabajo ocurre en el intérprete de PHP, antes de que intervenga siquiera el código del CMS o de la tienda. Por eso, los fallos en esta parte de PHP afectan a todos los sitios web, tengan o no un formulario de subida.

El proceso, en resumen:

  1. Se envía el formulario con method="POST" y enctype="multipart/form-data".
  2. El servidor web acepta la petición y se la pasa a PHP; según el modo de ejecución, mediante PHP-FPM, mod_php u otra interfaz.
  3. PHP comprueba límites como post_max_size y descompone el contenido a partir de una cadena separadora, la llamada boundary.
  4. Los campos normales acaban en $_POST; las partes de archivo se escriben en archivos temporales y se describen en $_FILES.
  5. Solo ahora se inicia el script, que comprueba el archivo y lo mueve a su destino.
  6. Al final de la petición, PHP elimina todos los archivos temporales que no se hayan movido.

multipart/form-data: la estructura de una petición de subida

Una petición de subida es texto sin formato con datos de archivo incrustados. La cabecera Content-Type indica la boundary y, en el contenido, esa cadena separa las distintas partes. Cada parte tiene sus propias líneas de cabecera con el nombre del campo y, en su caso, el nombre del archivo. Un ejemplo inofensivo y muy abreviado:

POST /bewerbung HTTP/1.1
Content-Type: multipart/form-data; boundary=----trenner42

------trenner42
Content-Disposition: form-data; name="vorname"

Erika
------trenner42
Content-Disposition: form-data; name="lebenslauf"; filename="lebenslauf.pdf"
Content-Type: application/pdf

%PDF-1.7 ... Dateiinhalt ...
------trenner42--

Todos los datos de este texto proceden del remitente, también el nombre del archivo y el Content-Type de la parte. PHP los incorpora a $_FILES, pero no comprueba su verosimilitud.

El array $_FILES

Para un campo llamado lebenslauf, $_FILES['lebenslauf'] contiene, según el manual de PHP, estas claves:

Clave Contenido ¿Es fiable?
name nombre original del archivo en el remitente no
full_path ruta comunicada por el navegador, desde PHP 8.1.0 no
type tipo MIME según el navegador, sin comprobación de PHP no
size tamaño en bytes lo determina PHP
tmp_name ruta del archivo temporal en el servidor la asigna PHP
error código de error del procesamiento de la subida lo establece PHP

En los campos con corchetes en el nombre, por ejemplo bilder[], cada clave contiene un array con una entrada por archivo. La aplicación recorre entonces, por ejemplo, $_FILES['bilder']['error'] y accede con el mismo índice a tmp_name y name.

Un formulario puede enviar además, antes del campo de archivo, un campo oculto MAX_FILE_SIZE. Según el manual, es solo una indicación para el navegador y se elude con facilidad. Solo puede confiar en los límites que impone el servidor: en PHP, en el servidor web y en la propia aplicación.

Límites: las directivas de php.ini para las subidas

Estas directivas controlan el procesamiento de las subidas. Los valores por defecto proceden del manual de PHP y se aplican cuando ningún php.ini los sobrescribe.

Directiva Valor por defecto Modificable en
file_uploads 1 solo configuración del sistema
upload_tmp_dir vacío (temporal del sistema) solo configuración del sistema
upload_max_filesize 2M php.ini, pool, configuración por directorio
post_max_size 8M php.ini, pool, configuración por directorio
max_file_uploads 20 php.ini, pool, configuración por directorio
enable_post_data_reading 1 php.ini, pool, configuración por directorio

post_max_size debe ser mayor que upload_max_filesize y, según el manual, memory_limit debería ser por lo general mayor que post_max_size. Si upload_tmp_dir no admite escritura, PHP recurre al directorio temporal del sistema. Con open_basedir, el directorio temporal debe estar permitido; de lo contrario, las subidas fallan. Los valores prácticos y sus consecuencias se describen en el artículo Limitar con criterio post_max_size y upload_max_filesize.

Interpretar correctamente los códigos de error

El campo error indica qué ha ocurrido durante el procesamiento. Las constantes van de UPLOAD_ERR_OK (0), pasando por UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) y UPLOAD_ERR_NO_FILE (4), hasta UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) y UPLOAD_ERR_EXTENSION (8).

<?php
declare(strict_types=1);

$datei = $_FILES['lebenslauf'] ?? null;
if ($datei === null) {
    exit('Keine Upload-Daten, eventuell wurde post_max_size überschritten.');
}

$meldung = match ($datei['error']) {
    UPLOAD_ERR_OK => null,
    UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Datei zu groß.',
    UPLOAD_ERR_NO_FILE => 'Keine Datei gewählt.',
    default => 'Upload fehlgeschlagen.',
};

if ($meldung === null && is_uploaded_file($datei['tmp_name'])) {
    $ziel = '/srv/app/uploads/' . bin2hex(random_bytes(16)) . '.pdf';
    move_uploaded_file($datei['tmp_name'], $ziel);
}

El ejemplo muestra solo la mecánica. La comprobación del tipo de archivo, el almacenamiento fuera del webroot y el bloqueo de la ejecución de scripts se describen en la guía Implementar con seguridad las subidas de archivos en PHP.

Subidas y PHP2Shell: qué significan las medidas provisionales

Las vulnerabilidades de PHP2Shell, CVE-2026-109618 y CVE-2026-109619, afectan al procesamiento que rodea a la petición dentro del intérprete de PHP. Por eso php.net indica como medidas provisionales, entre otras, post_max_size = 8M (sin subidas) o 32M (con subidas), upload_max_filesize por debajo de ese valor y session.upload_progress.enabled=0. Estos valores van en php.ini, en el pool de FPM o en la configuración del servidor, y después deben reiniciarse todos los workers.

Algunos autores especializados proponen además bloquear en el proxy o en el WAF las peticiones multipart/form-data que no se dirijan a rutas de subida auténticas. Es una medida no oficial, con el riesgo de que los formularios dejen de funcionar; el artículo Filtrar las peticiones de subida en el perímetro describe cómo sopesarla. Ninguna de estas medidas sustituye a la actualización a PHP 8.5.12, 8.4.27, 8.3.36 o 8.2.35.

¿Cómo ayuda PHP SOS en concreto?

  • Determinamos cuáles de sus sitios web necesitan realmente subidas y en qué rutas.
  • Fijamos los límites y las medidas provisionales por pool, reiniciamos todos los workers y probamos los formularios.
  • Revisamos los directorios de subida en busca de archivos ejecutables y bloqueamos la ejecución de scripts como parte del refuerzo del servidor.
  • Aplicamos la actualización de PHP y documentamos el estado de los parches de cada sitio web.

Preguntas frecuentes

¿Dónde guarda PHP primero los archivos subidos?

En un directorio temporal. Por defecto es el directorio temporal del sistema; se puede cambiar con upload_tmp_dir. Si el script no mueve el archivo, PHP lo elimina al final de la petición.

¿Puedo fiarme del campo type de $_FILES?

No. El tipo MIME lo comunica el navegador y, según el manual, PHP no lo comprueba. También name y full_path proceden del remitente. El tipo real del archivo lo determina usted en el servidor a partir del contenido.

¿Por qué $_FILES está vacío aunque se haya enviado un archivo?

Lo más habitual es que la petición completa fuera mayor que post_max_size. Entonces PHP vacía por completo $_POST y $_FILES, sin código de error en el array. Otras causas son que falte enctype en el formulario o que file_uploads esté desactivado.

¿Sirve de algo frente a PHP2Shell prohibir las subidas en la aplicación?

No. El procesamiento de los datos de la petición ocurre en PHP antes de que se ejecute el código de la aplicación. Lo eficaz es la actualización de PHP y el reinicio de todos los workers; unos límites más estrictos son solo una medida provisional.

¿Qué diferencia hay entre upload_max_filesize y post_max_size?

upload_max_filesize limita cada archivo por separado; post_max_size, el contenido total de la petición, incluidos todos los archivos y campos. Por eso post_max_size debe ser mayor que upload_max_filesize.

Fuentes

Contacto inmediato

Cada hora cuenta. Escríbanos ahora.

Describa brevemente el sistema, la versión de PHP y lo que ha observado. Nos pondremos en contacto, aclararemos la situación y trabajaremos en remoto hasta que el incidente quede cerrado y documentado.

Ayuda inmediata