Gestionar la subida de archivos en PHP: aumentar el tamaño y validar las subidas
Gestionar la subida de archivos es un requisito habitual en las aplicaciones web, pero plantea sus propios retos. Esta guía explora cómo gestionar la subida de archivos en PHP 8.3, incluyendo cómo comprobar y aumentar el tamaño máximo de subida, modificar la configuración de PHP, restringir los tipos de archivo y validar las subidas para mejorar la seguridad y la eficiencia de tus aplicaciones.
Versión de PHP y valores predeterminados de configuración
Esta guía está dirigida a PHP 8.3. En PHP 8.3, los ajustes predeterminados de subida de archivos suelen incluir:
upload_max_filesizeestablecido en 2M.post_max_sizenormalmente en torno a 8M (el valor puede variar según tu configuración).max_file_uploadsestablecido en 20.
Revisa tu configuración actual según sea necesario antes de hacer cambios.
Cómo comprobar el tamaño máximo de subida de archivos en PHP
En PHP 8.3, el valor predeterminado de upload_max_filesize es 2 MB y el de max_file_uploads es 20.
Antes de hacer cualquier cambio, es esencial conocer tus límites de subida actuales. PHP controla el
tamaño de las subidas de archivos con dos directivas de configuración: upload_max_filesize y post_max_size. Puedes
comprobar estos valores creando un sencillo script de PHP:
<?php
try {
echo 'upload_max_filesize: ' . ini_get('upload_max_filesize') . '<br>';
echo 'post_max_size: ' . ini_get('post_max_size') . '<br>';
echo 'max_file_uploads: ' . ini_get('max_file_uploads') . '<br>';
echo 'memory_limit: ' . ini_get('memory_limit') . '<br>';
} catch (Exception $e) {
error_log('Error checking PHP configuration: ' . $e->getMessage());
echo 'Error checking configuration settings.';
}
?>
Guarda este script como check_upload_size.php y ejecútalo en tu servidor. Mostrará el tamaño máximo
actual de subida de archivos y el tamaño máximo de post permitido.
Como alternativa, puedes usar la función phpinfo() para obtener una visión completa de tu
configuración de PHP:
<?php
phpinfo();
?>
Al acceder a este script desde tu navegador obtendrás información detallada sobre tu configuración de PHP.
Cómo aumentar el tamaño de subida de archivos en PHP
Si necesitas permitir subidas de archivos más grandes, tendrás que ajustar las directivas
upload_max_filesize y post_max_size en tu configuración de PHP. Así puedes hacerlo:
Usar .htaccess (para Apache 2.4+ con mod_php)
Añade las siguientes líneas a tu archivo .htaccess:
<IfModule mod_php.c>
php_value upload_max_filesize 20M
php_value post_max_size 25M
php_value memory_limit 256M
php_value max_execution_time 300
</IfModule>
Esto establece el tamaño máximo de archivo subido en 20 megabytes y el tamaño máximo de post en 25 megabytes. Ajusta los valores según tus necesidades.
Usar la configuración de PHP-FPM
Si usas PHP-FPM, añade estos ajustes a tu archivo de configuración de pool (normalmente en
/etc/php/8.3/fpm/pool.d/www.conf):
php_admin_value[upload_max_filesize] = 20M
php_admin_value[post_max_size] = 25M
php_admin_value[memory_limit] = 256M
Cómo cambiar el tamaño máximo de subida de archivos en php.ini en Ubuntu
Si tienes acceso al archivo php.ini de tu servidor, puedes hacer los cambios ahí. Así se hace
en un sistema Ubuntu:
-
Localiza tu archivo
php.ini. En PHP 8.3, suele estar en/etc/php/8.3/apache2/php.inio/etc/php/8.3/fpm/php.ini. -
Abre el archivo en un editor de texto con privilegios de root:
sudo nano /etc/php/8.3/apache2/php.ini -
Busca y modifica estas directivas:
upload_max_filesize = 20M post_max_size = 25M memory_limit = 256M max_execution_time = 300 max_file_uploads = 50 -
Guarda el archivo y sal del editor.
-
Reinicia tu servidor web y PHP-FPM (si corresponde):
sudo systemctl restart apache2 sudo systemctl restart php8.3-fpm
Implementar la funcionalidad de subida de archivos en PHP
Con la configuración lista, ya puedes implementar la funcionalidad de subida de archivos en tu aplicación.
Formulario HTML
Primero, crea un formulario HTML que permita a los usuarios subir archivos:
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">Choose file to upload:</label>
<input type="file" name="file" id="file" required />
<input type="submit" value="Upload" />
</form>
Asegúrate de establecer enctype="multipart/form-data" en la etiqueta del formulario para gestionar la subida de archivos.
Script de subida en PHP
Esta es una implementación de upload.php que usa características de PHP 8.3. Crea
/var/lib/php-upload-demo fuera del webroot, con el proceso de trabajo de PHP como propietario, y no lo
expongas a través del servidor web:
<?php
declare(strict_types=1);
class FileUploadHandler {
private string $uploadDirectory;
private array $allowedTypes;
private array $allowedExtensions;
private int $maxFileSize;
private array $uploadedFiles = [];
public function __construct(
string $uploadDirectory = '/var/lib/php-upload-demo/',
array $allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'],
array $allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf'],
int $maxFileSize = 5242880 // 5MB
) {
$this->uploadDirectory = rtrim($uploadDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->allowedTypes = $allowedTypes;
$this->allowedExtensions = $allowedExtensions;
$this->maxFileSize = $maxFileSize;
$this->initializeUploadDirectory();
}
private function initializeUploadDirectory(): void {
if (!is_dir($this->uploadDirectory)) {
if (!mkdir($this->uploadDirectory, 0700, true)) {
throw new RuntimeException('Failed to create upload directory');
}
}
}
public function handleUpload(): array {
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new RuntimeException('Invalid request method');
}
if (!isset($_FILES['file'])) {
throw new RuntimeException('No file uploaded');
}
$file = $_FILES['file'];
$this->validateUpload($file);
$fileInfo = $this->processUpload($file);
$this->uploadedFiles[] = $fileInfo;
return [
'success' => true,
'message' => 'File uploaded successfully',
'file' => $fileInfo
];
} catch (Exception $e) {
error_log('Upload error: ' . $e->getMessage());
return [
'success' => false,
'message' => 'The file could not be uploaded. Check its type and size.'
];
}
}
private function validateUpload(array $file): void {
if (!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'])) {
throw new RuntimeException('Invalid upload fields');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException($this->getUploadErrorMessage($file['error']));
}
if ($file['size'] > $this->maxFileSize) {
throw new RuntimeException('File is too large');
}
$fileType = mime_content_type($file['tmp_name']);
if (!in_array($fileType, $this->allowedTypes, true)) {
throw new RuntimeException('Invalid file type');
}
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $this->allowedExtensions, true)) {
throw new RuntimeException('Invalid file extension');
}
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('Invalid upload attempt');
}
}
private function processUpload(array $file): array {
$hash = hash_file('sha256', $file['tmp_name']);
$extensions = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
$extension = $extensions[mime_content_type($file['tmp_name'])] ?? null;
if ($extension === null) {
throw new RuntimeException('No storage extension configured for this type');
}
$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $this->uploadDirectory . $newFilename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Failed to move uploaded file');
}
chmod($destination, 0600);
return [
'name' => $file['name'],
'type' => mime_content_type($destination),
'size' => filesize($destination),
'hash' => $hash,
'filename' => $newFilename
];
}
private function getUploadErrorMessage(int $error): string {
return match($error) {
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'File upload stopped by extension',
default => 'Unknown upload error'
};
}
}
// Usage
header('Content-Type: application/json');
try {
$handler = new FileUploadHandler();
echo json_encode($handler->handleUpload(), JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
error_log('Upload handler failed: ' . get_class($e));
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'The upload service is unavailable.']);
}
Esta implementación incluye:
- Comprobación estricta de tipos
- Gestión integral de errores con try-catch
- Un hash SHA-256 para comparaciones de integridad posteriores
- Detección del tipo de archivo con la extensión Fileinfo, que no demuestra que el contenido sea inofensivo
- Almacenamiento privado fuera del webroot con permisos restrictivos
- Protección frente a vulnerabilidades comunes de subida
Buenas prácticas para subidas de archivos seguras
Seguridad de directorios
-
Establece los permisos adecuados:
chmod 700 /var/lib/php-upload-demo/ find /var/lib/php-upload-demo/ -type f -exec chmod 600 {} \; -
Si usas
open_basedir, incluye los directorios de aplicación, de almacenamiento y temporales que requiera tu despliegue. Por ejemplo:open_basedir = /var/www/app/:/var/lib/php-upload-demo/:/tmp/ -
Organiza las subidas en subdirectorios aleatorios para aislar los archivos y reducir la exposición a ataques de salto de directorio.
-
Implementa la limitación de tasa:
<?php session_start(); if (!isset($_SESSION['upload_count'])) { $_SESSION['upload_count'] = 0; $_SESSION['upload_time'] = time(); } if (time() - $_SESSION['upload_time'] > 3600) { $_SESSION['upload_count'] = 0; $_SESSION['upload_time'] = time(); } if ($_SESSION['upload_count'] >= 10) { throw new RuntimeException('Upload limit exceeded'); } $_SESSION['upload_count']++;
Análisis de virus
Envía el contenido de los archivos a ClamAV con INSTREAM. Así, los directorios privados 0700 y los
archivos 0600 anteriores se mantienen intactos incluso cuando el demonio se ejecuta con otra
cuenta:
<?php
function scanFile(string $filepath): bool {
$path = realpath($filepath);
if ($path === false || !is_file($path)) {
return false;
}
$file = @fopen($path, 'rb');
if ($file === false) {
return false;
}
$socket = @stream_socket_client('unix:///var/run/clamav/clamd.ctl', $errno, $error, 5);
if ($socket === false) {
fclose($file);
return false;
}
stream_set_timeout($socket, 30);
$writeAll = function (string $bytes) use ($socket): bool {
while ($bytes !== '') {
$written = @fwrite($socket, $bytes);
if ($written === false || $written === 0) {
return false;
}
$bytes = substr($bytes, $written);
}
return true;
};
try {
if (!$writeAll("zINSTREAM\0")) {
return false;
}
while (!feof($file)) {
$chunk = @fread($file, 8192);
if ($chunk === false || ($chunk === '' && !feof($file))) {
return false;
}
if ($chunk !== '' && !$writeAll(pack('N', strlen($chunk)) . $chunk)) {
return false;
}
}
if (!$writeAll(pack('N', 0))) {
return false;
}
$result = '';
while (strlen($result) < 4096) {
$byte = @fread($socket, 1);
if ($byte === false || $byte === '') {
return false;
}
if ($byte === "\0") {
return $result === 'stream: OK';
}
$result .= $byte;
}
return false;
} finally {
fclose($file);
fclose($socket);
}
}
?>
ClamAV debe estar en ejecución y PHP necesita permiso para conectarse a su socket local de
confianza. El demonio no necesita acceso al directorio de subidas. Configura StreamMaxLength y los
límites de análisis del motor para los tamaños de subida que permitas; consulta la
documentación del protocolo ClamD.
Trata un resultado false como un rechazo, incluidos los escáneres no disponibles y las respuestas
incompletas. Llama al escáner antes de liberar un archivo subido y no lo modifiques entre el
análisis y la liberación.
Integración con frameworks modernos
Para una gestión más robusta, considera usar frameworks modernos de PHP. Por ejemplo, un método de controlador de Laravel podría verse así:
<?php
// Laravel example
use Illuminate\Http\Request;
class UploadController extends \Illuminate\Routing\Controller {
public function upload(Request $request) {
$request->validate([
'file' => 'required|file|mimes:pdf,jpg,png|max:5120'
]);
$path = $request->file('file')->store('uploads');
return response()->json(['success' => true, 'path' => $path]);
}
}
Técnicas avanzadas de subida
Más allá de lo básico, puedes mejorar aún más la experiencia de subida y afrontar escenarios más exigentes:
-
Gestión de subidas AJAX: usa XMLHttpRequest para subir archivos de forma asíncrona y seguir el progreso de la subida. Fetch no expone eventos equivalentes de progreso de subida. Por ejemplo:
<script> const fileInput = document.getElementById('file') fileInput.addEventListener('change', function () { const file = fileInput.files[0] const xhr = new XMLHttpRequest() xhr.upload.addEventListener('progress', function (e) { if (e.lengthComputable && e.total > 0) { const percent = (e.loaded / e.total) * 100 console.log('Upload progress: ' + percent.toFixed(2) + '%') } }) xhr.open('POST', 'upload.php') const formData = new FormData() formData.append('file', file) xhr.send(formData) }) </script> -
Subidas por fragmentos para archivos grandes: para archivos muy grandes, considera dividir el archivo en fragmentos más pequeños y subirlos de forma secuencial. Este método puede mejorar la fiabilidad y permitir subidas reanudables.
-
Compatibilidad con más formatos de archivo: amplía tu lógica de validación para admitir otros formatos, como imágenes WebP o archivos comprimidos ZIP. Realiza siempre una validación estricta para garantizar que el contenido del archivo coincide con su extensión.
Conclusión
Siguiendo estas técnicas, puedes gestionar la subida de archivos en PHP de forma segura y eficiente. Desde aumentar los límites de tamaño de subida y aplicar una validación robusta hasta implementar métodos avanzados como las subidas AJAX y las transferencias por fragmentos, estas prácticas ayudan a mejorar tanto la seguridad como la experiencia de usuario. Para una solución integrada con herramientas modernas de subida de archivos, considera explorar el servicio de subida de archivos de Transloadit.
