Aumenta el tamaño de subida PHP e identifica el límite de rechazo
Para aceptar un archivo de 20 MiB, configura upload_max_filesize = 20M, deja más espacio para
la solicitud multipart completa con post_max_size = 25M y asegúrate de que tu aplicación
y tu servidor web también lo permitan. Cambiar la configuración de PHP no puede anular un límite
menor en tu manejador de subidas. Esta guía muestra cómo encontrar la configuración activa y
reproducir cada fallo por HTTP con un pequeño endpoint local.
Cómo comprobar el tamaño máximo de subida de archivos en PHP
Comprueba el proceso de PHP que atiende la URL de subida. php --ini informa
sobre la configuración de la línea de comandos; no determina qué configuración cargaron FPM o
Apache. PHP puede usar
distintos archivos de configuración para distintas interfaces de servidor.
Guarda esto como check_upload_size.php junto a tu endpoint de subida y solicítalo a través
del mismo sitio web. En un sitio existente, restringe el acceso a administradores y elimínalo
tras el diagnóstico: las rutas de los archivos te resultan útiles, pero no deben ser públicas.
<?php
header('Content-Type: application/json');
echo json_encode([
'version' => PHP_VERSION,
'sapi' => PHP_SAPI,
'loaded_ini' => php_ini_loaded_file(),
'scanned_ini' => php_ini_scanned_files(),
'file_uploads' => ini_get('file_uploads'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'max_file_uploads' => ini_get('max_file_uploads'),
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
PHP documenta valores predeterminados de 2M para
upload_max_filesize, 8M para post_max_size y
20 para max_file_uploads; cada instalación puede sobrescribirlos. Los dos primeros
son límites en bytes, mientras que max_file_uploads limita la cantidad de archivos.
En la notación de tamaños de PHP, 20M significa 20 × 1.024 × 1.024 bytes,
o 20 MiB. Consulta las directivas principales y el
analizador de tamaños.
| Capa | Qué limita | Qué buscar |
|---|---|---|
| Servidor web o proxy | El cuerpo de la solicitud entrante | Un rechazo antes de que se ejecute el manejador de PHP; revisa los registros de ese servidor |
post_max_size | Todo el cuerpo del POST, incluidos los campos y los datos adicionales de multipart | Tanto $_POST como $_FILES pueden estar vacíos |
upload_max_filesize | Cada archivo subido | UPLOAD_ERR_INI_SIZE en el campo error del archivo |
| Validación de la aplicación | El archivo que aceptará tu aplicación | Una constante del manejador o una regla del framework que puede ser menor que el límite de PHP |
PHP documenta el comportamiento de las superglobales vacías
y los códigos de error de subida.
Un $_FILES vacío por sí solo no demuestra que la solicitud sea demasiado
grande: la ausencia de un campo de archivo también puede producirlo.
Cómo aumentar el tamaño de subida de archivos en PHP
Edita la configuración que usa la solicitud web y utiliza la salida del diagnóstico anterior
para localizarla. En Ubuntu, una instalación de PHP 8.3 mediante paquetes suele usar
/etc/php/8.3/fpm/php.ini para FPM o /etc/php/8.3/apache2/php.ini para mod_php.
La ruta indicada y los valores efectivos tienen prioridad sobre esos ejemplos.
file_uploads = On
upload_max_filesize = 20M
post_max_size = 25M
display_errors = Off
log_errors = On
Recarga el servicio que ejecuta PHP después de cambiar su configuración. Con FPM, se trata del
servicio FPM correspondiente; con mod_php, de Apache. Vuelve a solicitar la URL de diagnóstico y
verifica los valores efectivos. Estas dos directivas de tamaño son opciones
INI_PERDIR, por lo que ini_set() dentro de
upload.php no puede aumentarlas. Desactivar la visualización de errores en la
configuración también evita que las advertencias emitidas durante el análisis de la solicitud
aparezcan en la respuesta JSON.
Uso de la configuración de PHP-FPM
Un pool puede sobrescribir php.ini. Revisa el pool que atiende este sitio
si los valores siguen siendo distintos. Para un límite de 20 MiB, las opciones correspondientes
del pool son php_admin_value[upload_max_filesize] = 20M y php_admin_value[post_max_size] = 25M.
La referencia de configuración de FPM
explica cómo sobrescribir valores desde el pool. Recarga el servicio FPM afectado y vuelve a
comprobarlos por HTTP.
Uso de .htaccess (para Apache 2.4+ con mod_php)
Cuando PHP se ejecuta como módulo de Apache, php_value upload_max_filesize 20M y
php_value post_max_size 25M pueden incluirse en .htaccess si el servidor
permite sobrescribir esos valores. No son opciones de PHP-FPM; no las añadas solo porque Apache
sea el servidor frontal del sitio. Consulta las
instrucciones de configuración de Apache de PHP.
Si la solicitud nunca llega a PHP, comprueba el límite del cuerpo en las capas anteriores.
La directiva client_max_body_size de Nginx tiene un valor
predeterminado de 1m y devuelve 413 si la solicitud es demasiado
grande. En este ejemplo, client_max_body_size 25m; permite la solicitud multipart prevista.
Apache dispone de LimitRequestBody, y un proxy del
alojamiento puede imponer otro límite. Deja espacio para toda la solicitud en cada capa.
Aumentar un límite de PHP no modifica ninguna de estas opciones.
Implementación de la subida de archivos en PHP
Usa un entorno Linux local con PHP 8.3 o posterior de 64 bits, la extensión Fileinfo y cURL. El ejemplo se probó con PHP 8.3.30 y 8.5.10. El servidor integrado de PHP está destinado al desarrollo local. No reproduce un despliegue con FPM, Apache o un proxy.
Crea un directorio nuevo. Los comandos encadenados se detienen si el directorio ya existe o si falla el cambio de directorio; elige otro nombre en lugar de sobrescribir un proyecto existente.
mkdir php-upload-demo &&
cd php-upload-demo &&
mkdir public private &&
chmod 700 private
Continúa solo cuando esto termine correctamente. Guarda el bloque INI anterior como
php-upload.ini en este directorio. Guarda el script de diagnóstico como
public/check_upload_size.php.
Script de subida en PHP
Guarda este manejador completo como public/upload.php. APP_MAX_BYTES
es el límite independiente de la aplicación; aquí coincide con el límite de PHP de 20 MiB por
archivo. El manejador acepta archivos JPEG, PNG y PDF, comprueba que el tipo MIME detectado
coincida con la extensión del nombre de archivo y conserva los bytes aceptados en
private/, fuera de la raíz de documentos.
<?php
declare(strict_types=1);
const APP_MAX_BYTES = 20 * 1024 * 1024;
header('Content-Type: application/json');
function rejectUpload(int $status, string $code, string $message): never {
http_response_code($status);
echo json_encode([
'success' => false,
'code' => $code,
'message' => $message,
], JSON_THROW_ON_ERROR);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
rejectUpload(405, 'method', 'Send a multipart POST request.');
}
$postLimit = ini_parse_quantity(ini_get('post_max_size'));
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? null;
if ($postLimit > 0 && $contentLength !== null && (int) $contentLength > $postLimit) {
rejectUpload(413, 'post_max_size', 'The complete request exceeds PHP post_max_size.');
}
$file = $_FILES['file'] ?? null;
if (!is_array($file) || array_keys($_FILES) !== ['file'] ||
!isset($file['error'], $file['size'], $file['name'], $file['tmp_name']) ||
!is_int($file['error']) || !is_int($file['size']) ||
!is_string($file['name']) || !is_string($file['tmp_name'])) {
rejectUpload(400, 'invalid_upload', 'Send one file in the file field, without array brackets.');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
[$status, $code, $message] = match ($file['error']) {
UPLOAD_ERR_INI_SIZE => [413, 'upload_max_filesize', 'The file exceeds PHP upload_max_filesize.'],
UPLOAD_ERR_FORM_SIZE => [413, 'form_limit', 'The file exceeds the submitted MAX_FILE_SIZE.'],
UPLOAD_ERR_PARTIAL => [400, 'partial_upload', 'The file arrived incomplete. Retry the upload.'],
UPLOAD_ERR_NO_FILE => [400, 'missing_file', 'Choose a file to upload.'],
default => [500, 'upload_unavailable', 'The upload service is unavailable.'],
};
rejectUpload($status, $code, $message);
}
if (!is_uploaded_file($file['tmp_name'])) {
rejectUpload(400, 'invalid_upload', 'The file is not a valid HTTP upload.');
}
$directory = null;
$destination = null;
try {
$size = filesize($file['tmp_name']);
if ($size === false) {
throw new RuntimeException('Cannot measure upload');
}
if ($size === 0) {
rejectUpload(400, 'empty_file', 'The file is empty.');
}
if ($size > APP_MAX_BYTES) {
rejectUpload(413, 'application_limit', 'The file exceeds the application size limit.');
}
$allowed = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'application/pdf' => ['pdf'],
];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!isset($allowed[$mime]) || !in_array($extension, $allowed[$mime], true)) {
rejectUpload(415, 'file_type', 'Send a JPEG, PNG, or PDF with a matching extension.');
}
$hash = hash_file('sha256', $file['tmp_name']);
if ($hash === false) {
throw new RuntimeException('Cannot hash upload');
}
$id = bin2hex(random_bytes(16));
$candidate = dirname(__DIR__) . '/private/' . $id;
// Creating a new directory reserves this ID without overwriting an earlier upload.
if (!@mkdir($candidate, 0700)) {
throw new RuntimeException('Cannot reserve storage');
}
$directory = $candidate;
$destination = $directory . '/file.' . $allowed[$mime][0];
if (!@move_uploaded_file($file['tmp_name'], $destination) || !@chmod($destination, 0600)) {
throw new RuntimeException('Cannot store upload');
}
http_response_code(201);
echo json_encode([
'success' => true,
'id' => $id,
'type' => $mime,
'size' => $size,
'sha256' => $hash,
], JSON_THROW_ON_ERROR);
} catch (Throwable $error) {
if ($destination !== null) {
@unlink($destination);
}
if ($directory !== null) {
@rmdir($directory);
}
error_log('Upload failed: ' . get_class($error));
rejectUpload(500, 'upload_unavailable', 'The upload service is unavailable.');
}
La comprobación del tamaño de la solicitud usa Content-Length para distinguir un
cuerpo cuyo tamaño excesivo se conoce de la ausencia de un archivo. Las solicitudes cURL que
aparecen a continuación proporcionan esa longitud. Sin ella, el manejador no puede diagnosticar
así que se haya superado post_max_size; aplica un límite al cuerpo en el servidor
web para los clientes que envían solicitudes en streaming. ini_parse_quantity() maneja
los sufijos de tamaño de PHP, incluido un límite de cero, que desactiva el límite de tamaño de
POST multipart.
Inicia el servidor desde php-upload-demo. Elige un puerto disponible y usa ese
mismo puerto en las siguientes solicitudes. Este comando sirve únicamente
public/:
php -c php-upload.ini -S 127.0.0.1:8080 -t public
Deja esa terminal en ejecución. En una segunda terminal, abre el mismo directorio del proyecto y comprueba lo siguiente:
curl -fsS http://127.0.0.1:8080/check_upload_size.php
El valor de sapi debe ser cli-server, el de
upload_max_filesize debe ser 20M y el de
post_max_size debe ser 25M.
La ruta del INI cargado debe apuntar a este proyecto. Detén el servidor con Ctrl+C al terminar.
Envía solicitudes multipart reales
Para realizar una prueba de tamaño reproducible, guarda esto como make-probe.php
en la raíz del proyecto. Crea un PNG válido de un píxel con relleno hasta alcanzar el tamaño en
bytes solicitado. El relleno es deliberado: permite probar los límites en bytes sin necesitar
una fotografía grande y demuestra por qué la detección MIME no constituye un dictamen de
seguridad. El modo de creación exclusiva impide sobrescribir un archivo de prueba existente.
<?php
declare(strict_types=1);
$bytes = filter_var($argv[2] ?? '', FILTER_VALIDATE_INT);
if ($argc !== 3 || $bytes === false || $bytes < 1024 || $bytes > 30 * 1024 * 1024) {
fwrite(STDERR, "Usage: php make-probe.php OUTPUT BYTES (1024 to 31457280)\n");
exit(1);
}
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==', true);
$output = @fopen($argv[1], 'xb');
if ($output === false) {
fwrite(STDERR, "Cannot create probe; choose a new output filename.\n");
exit(1);
}
if (fwrite($output, $png) !== strlen($png) || !ftruncate($output, $bytes) || !fclose($output)) {
fwrite(STDERR, "Cannot finish probe.\n");
exit(1);
}
Crea y envía un archivo de 6 MiB. -F proporciona la codificación
multipart y su delimitador; no establezcas tú mismo la cabecera Content-Type.
-i muestra el estado de la respuesta. Estas llamadas de diagnóstico
omiten --fail de cURL para que puedas leer el cuerpo JSON de las
respuestas de error esperadas.
php make-probe.php probe-6.png 6291456 &&
curl -sS -i -F 'file=@probe-6.png' http://127.0.0.1:8080/upload.php
La respuesta esperada es HTTP 201 con success: true,
type: "image/png", size: 6291456, un id
aleatorio y un sha256. Los bytes permanecen en
private/<id>/file.png; no hay una URL de descarga pública. Compara el hash de la
respuesta con un hash local del archivo de origen:
php -r 'echo hash_file("sha256", "probe-6.png"), PHP_EOL;'
Repite la prueba con el límite exacto de 20 MiB y luego con archivos lo bastante grandes como para activar cada límite de PHP:
php make-probe.php probe-20.png 20971520 &&
curl -sS -i -F 'file=@probe-20.png' http://127.0.0.1:8080/upload.php
php make-probe.php probe-21.png 22020096 &&
curl -sS -i -F 'file=@probe-21.png' http://127.0.0.1:8080/upload.php
php make-probe.php probe-26.png 27262976 &&
curl -sS -i -F 'file=@probe-26.png' http://127.0.0.1:8080/upload.php
| Archivo | Resultado esperado con la configuración documentada |
|---|---|
| PNG de 6 MiB | 201; almacenado con bytes y hash coincidentes |
| PNG de 20 MiB | 201; el límite por archivo es inclusivo |
| PNG de 21 MiB | 413 con code: "upload_max_filesize" |
| PNG de 26 MiB | 413 con code: "post_max_size"; la solicitud completa es demasiado grande |
Para aislar el límite de la aplicación, detén el servidor y reinícialo con esta modificación temporal:
php -c php-upload.ini -d upload_max_filesize=24M -S 127.0.0.1:8080 -t public
Vuelve a enviar el archivo de prueba existente de 21 MiB:
curl -sS -i -F 'file=@probe-21.png' http://127.0.0.1:8080/upload.php
Ahora la respuesta esperada es 413 con code: "application_limit": PHP permite el archivo,
pero APP_MAX_BYTES lo rechaza. Después, restaura el comando original del
servidor. Si una aplicación existente sigue rechazando un archivo de 6 MiB después de que
hayas aumentado los límites de PHP, busca un límite menor en la aplicación, como
5242880 bytes, o una regla de validación del framework. Cambiar
php.ini no actualiza esa regla.
Los comandos de prueba nunca reemplazan un archivo de prueba existente. Para reenviarlo, ejecuta únicamente su comando cURL. Cada solicitud exitosa crea una nueva subida privada, aunque el contenido sea idéntico; los fallos no dejan ninguna subida almacenada. La demostración conserva las subidas exitosas hasta que elimines sus datos privados.
Buenas prácticas para subir archivos de forma segura
Seguridad de los directorios
Mantén private/ fuera del directorio servido y con la cuenta de PHP como
propietaria. Cada directorio reservado para una subida tiene el modo
0700; los archivos almacenados tienen el modo
0600. El nombre original del archivo nunca se convierte en una ruta
de almacenamiento. La función
move_uploaded_file() de PHP comprueba que el archivo
de origen se haya subido a través de PHP, pero puede sobrescribir un destino existente.
En este ejemplo, reservar un directorio aleatorio nuevo antes de mover el archivo evita esa
sobrescritura.
Antes de exponer un endpoint de subida
Este ejemplo local no tiene autenticación, cuotas de usuario, un escáner de malware ni servicio público de archivos. Fileinfo identifica un tipo probable a partir de los bytes; ni su resultado ni un hash SHA-256 demuestran que un archivo sea inofensivo. Para una aplicación desplegada, decide quién puede subir archivos, limita su uso del almacenamiento y valida o analiza el contenido antes de permitir el acceso a él. Los frameworks y los indicadores de progreso de subida siguen sujetos a los mismos límites de solicitudes y archivos; no los eliminan.
