La funcionalidad de subir archivos (o upload) es el corazón de cualquier aplicación web moderna, desde un simple formulario de contacto con attachments hasta plataformas completas de gestión de documentos e imágenes. Sin embargo, si se implementa incorrectamente, la carga de archivos se convierte en la puerta de entrada más común para ataques de seguridad en PHP.
En esta guía, no solo aprenderás el código básico para subir archivos con PHP, sino que te convertirás en un experto al implementar validaciones críticas, manejo de errores profesional e integración con MySQL que superan a la mayoría de los tutoriales básicos que encontrarás en línea.
Si estás buscando el código PHP para subir archivos al servidor de una manera robusta y a prueba de errores, has llegado al lugar correcto.
1. Introducción: ¿Por qué es Vital Subir Archivos de Forma Segura?
La mayoría de los desarrolladores novatos cometen errores al subir archivos que abren huecos de seguridad como:
Ejecución de Código Malicioso: Permitir que un atacante suba un archivo .php o .exe y luego lo ejecute en tu servidor.
Ataques de Filesystem Traversal: Permitir rutas relativas en el nombre del archivo para escribirlo en directorios fuera de tu carpeta de upload.
Sobrecarga del Servidor (DoS): No validar el tamaño, permitiendo que un usuario suba archivos gigantes que consuman tu ancho de banda y almacenamiento.
Nuestro objetivo aquí es darte un código funcional y, sobre todo, seguro.
2. El Formulario HTML Indispensable para la Carga
Todo proceso de upload comienza en el front-end con un formulario HTML perfectamente configurado.
El siguiente código HTML proporciona la estructura mínima:
<form action="procesar_upload.php" method="POST" enctype="multipart/form-data">
<h2>Subir Archivo al Servidor</h2>
<label for="archivo_subido">Seleccionar Archivo:</label>
<input type="file" name="archivo_subido" id="archivo_subido" required>
<button type="submit" name="submit">Subir Archivo</button>
</form>
El Atributo enctype: La Clave del Envío Binario
La parte más crítica del formulario es el atributo enctype. Si olvidas incluir enctype="multipart/form-data", tu script de PHP nunca podrá acceder a la información del archivo.
Este atributo le indica al navegador que debe codificar los datos del formulario de una manera especial que soporte el envío de datos binarios (el archivo en sí), no solo de texto plano. Sin él, la superglobal $_FILES aparecerá vacía.
3. El Corazón del Proceso: La Superglobal $_FILES
Cuando un usuario envía un formulario con enctype="multipart/form-data", PHP automáticamente crea una variable superglobal llamada $_FILES. Esta variable es un array asociativo que contiene toda la información temporal del archivo cargado.
La estructura de $_FILES para un campo llamado archivo_subido es la siguiente:
PHP
$_FILES['archivo_subido'] = [
'name' => 'nombre_original_del_archivo.pdf', // Nombre original del cliente.
'type' => 'application/pdf', // Tipo MIME del archivo.
'size' => 123456, // Tamaño en bytes.
'tmp_name' => '/tmp/phpXyZaBc', // Ruta temporal en el servidor.
'error' => 0 // Código de error (0 = Sin error).
];
| Clave de $_FILES | Descripción | Uso Práctico |
name | El nombre original que tenía el archivo en la computadora del cliente. | Útil para guardarlo con un nombre legible (¡pero hay que sanearlo!). |
type | El tipo MIME (ej. image/jpeg, application/pdf). Lo reporta el navegador. | Importante para validar el tipo de archivo. |
size | El tamaño del archivo en bytes. | Esencial para validar el tamaño máximo permitido. |
tmp_name | La ruta temporal donde PHP guardó el archivo en el servidor. | ¡Esta es la ruta que usas en move_uploaded_file()! |
error | Un código numérico que indica el estado de la carga. | CRÍTICO para saber si la subida fue exitosa. |
4. Tutorial Paso a Paso: El Script PHP Básico y Seguro
A continuación, el script mínimo, seguro y comentado para el procesamiento del archivo. (Archivo: procesar_upload.php)
PHP
<?php
// Deshabilitar la visualización de errores en producción por seguridad
ini_set('display_errors', 0);
error_reporting(0);
// Comprobación inicial de que el formulario ha sido enviado
if (isset($_POST['submit'])) {
// ----------------------------------------------------
// 4.1. Definir la Carpeta de Destino (¡CRÍTICO!)
// ----------------------------------------------------
$directorio_destino = "uploads/";
// Asegurarse de que el directorio existe. Si no, intenta crearlo.
// 0755: Permisos de lectura/escritura/ejecución para el dueño, lectura/ejecución para grupo y otros.
if (!is_dir($directorio_destino)) {
if (!mkdir($directorio_destino, 0755, true)) {
die("Error: No se pudo crear el directorio de destino ($directorio_destino). Verifique permisos.");
}
}
// Datos del archivo cargado (tomados de la superglobal $_FILES)
$nombre_original = $_FILES['archivo_subido']['name'];
$nombre_temporal = $_FILES['archivo_subido']['tmp_name'];
$codigo_error = $_FILES['archivo_subido']['error'];
// ----------------------------------------------------
// 4.2. Validación del Error
// ----------------------------------------------------
if ($codigo_error !== UPLOAD_ERR_OK) {
// En la Sección 5, te mostraremos cómo manejar todos los errores.
echo "Error en la carga: Código de error $codigo_error. La carga no se completó.";
} else {
// ----------------------------------------------------
// 4.3. SANITIZACIÓN: Generar un nombre único y seguro
// ----------------------------------------------------
// Siempre es buena práctica renombrar el archivo para evitar:
// 1. Colisiones de nombres (que dos usuarios suban el mismo nombre).
// 2. Ejecución de código si el nombre contiene caracteres peligrosos.
$extension = pathinfo($nombre_original, PATHINFO_EXTENSION);
$nombre_unico = uniqid() . '.' . $extension; // Ej: 65f3a745c1d.pdf
// Ruta final completa donde se moverá el archivo
$ruta_final = $directorio_destino . $nombre_unico;
// ----------------------------------------------------
// 4.4. La Función move_uploaded_file()
// ----------------------------------------------------
// Esta función comprueba que el archivo realmente fue subido por POST
// y lo mueve del directorio temporal a la ubicación final.
if (move_uploaded_file($nombre_temporal, $ruta_final)) {
echo "¡Éxito! El archivo **$nombre_original** ha sido subido con el nombre seguro **$nombre_unico** a $directorio_destino.";
} else {
echo "Error: Ocurrió un problema al mover el archivo. Verifique permisos del directorio 'uploads'.";
}
}
}
?>
La Función move_uploaded_file(): Seguridad Integrada
Es vital entender que siempre debes usar move_uploaded_file().
Esta función no solo mueve el archivo, sino que internamente verifica que el archivo en la ruta temporal (tmp_name) haya sido realmente cargado a través de un request HTTP POST. Usar copy() o rename() en su lugar, sin esta verificación, es un riesgo de seguridad que permite a un atacante engañar al script para que procese cualquier archivo de tu sistema.
5. Fortificación y Seguridad: Las 3 Validaciones Críticas (¡La Brecha!)
Para pasar de un código funcional a un código profesional y seguro, debes implementar al menos estas tres validaciones antes de llamar a move_uploaded_file().
Validación #1: Tipo y Extensión (Doble Chequeo)
Validar solo la extensión (.jpg, .pdf) es insuficiente, ya que un atacante puede renombrar un archivo malicioso (como un script PHP) a, por ejemplo, ataque.jpg. Debes validar también el Tipo MIME o, mejor aún, usar una lista blanca de extensiones permitidas.
Ejemplo de Validación Segura (Solo Imágenes JPG y PNG):
PHP
// Lista blanca de extensiones y tipos MIME permitidos
$extensiones_permitidas = ['jpg', 'jpeg', 'png', 'gif'];
$tipos_mime_permitidos = ['image/jpeg', 'image/png', 'image/gif'];
// Obtener la extensión y el tipo MIME del archivo
$extension_real = strtolower(pathinfo($nombre_original, PATHINFO_EXTENSION));
$tipo_mime = $_FILES['archivo_subido']['type'];
// Verificación
if (!in_array($extension_real, $extensiones_permitidas) || !in_array($tipo_mime, $tipos_mime_permitidos)) {
die("Error de Seguridad: Solo se permiten archivos JPG, PNG o GIF.");
}
Validación #2: Tamaño Máximo (Alineado con php.ini)
Valida que el archivo no exceda el tamaño máximo que deseas permitir. Esto evita que los usuarios saturen tu servidor con archivos enormes.
Ejemplo de Validación de Tamaño (Máximo 5MB):
PHP
// 5MB en bytes (1024 * 1024 * 5)
$tamano_maximo = 5242880;
if ($_FILES['archivo_subido']['size'] > $tamano_maximo) {
die("Error: El archivo excede el tamaño máximo permitido de 5MB.");
}
Validación #3: Sanitizar el Nombre del Archivo (Prevención de Ataques)
Esta es la validación que a menudo se omite. Si el nombre de archivo enviado por el usuario es usado sin cambios, puede contener caracteres peligrosos (como ../ o caracteres especiales) o incluso código. Por eso, en nuestro ejemplo básico usamos uniqid() y la extensión limpia.
Opción 1: Renombrar Completamente (Recomendado)
Usar uniqid() o un hash (como SHA-1) para generar un nombre totalmente nuevo y seguro, como hicimos en el código básico.
Opción 2: Limpieza Extrema (Si necesitas preservar el nombre)
Si necesitas mantener el nombre original, debes limpiarlo de todo carácter que no sea alfanumérico, un guion o un guion bajo.
PHP
// Ejemplo de limpieza (si no usas uniqid):
$nombre_limpio = preg_replace('/[^a-zA-Z0-9\-\.]/', '', $nombre_original);
$ruta_final = $directorio_destino . $nombre_limpio;
6. Manejo Profesional de Errores: La Variable $_FILES['error']
La clave error de la variable $_FILES es un código numérico. Al gestionarlos, proporcionas una mejor retroalimentación al usuario y controlas los fallos. El valor 0 es UPLOAD_ERR_OK (éxito). Cualquier otro valor es un error.
| Constante PHP | Valor | Descripción del Error |
UPLOAD_ERR_INI_SIZE | 1 | El archivo subido excede la directiva upload_max_filesize en php.ini. |
UPLOAD_ERR_FORM_SIZE | 2 | El archivo subido excede el límite especificado en el formulario HTML (MAX_FILE_SIZE). |
UPLOAD_ERR_PARTIAL | 3 | El archivo fue solo parcialmente subido. |
UPLOAD_ERR_NO_FILE | 4 | No se subió ningún archivo (el usuario no seleccionó nada). |
UPLOAD_ERR_NO_TMP_DIR | 6 | Falta la carpeta temporal del servidor. |
UPLOAD_ERR_CANT_WRITE | 7 | El servidor falló al escribir el archivo en el disco. |
UPLOAD_ERR_EXTENSION | 8 | Una extensión de PHP detuvo la carga. |
Manejo de Errores con Switch Case:
PHP
switch ($codigo_error) {
case UPLOAD_ERR_OK:
// El proceso de validación y movimiento continúa...
break;
case UPLOAD_ERR_NO_FILE:
die('Error 4: Por favor, selecciona un archivo antes de enviar.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
die('Error 1/2: El archivo es demasiado grande. Máximo permitido: ' . ini_get('upload_max_filesize'));
case UPLOAD_ERR_PARTIAL:
die('Error 3: La carga del archivo fue interrumpida. Intenta de nuevo.');
default:
die('Error desconocido en la carga. Código: ' . $codigo_error);
}
7. Caso Práctico Esencial: Subir Archivos a Carpeta y Registrar en MySQL
Esta sección satisface la alta demanda de la keyword subir archivos php mysql. El enfoque es:
Subir el archivo físico a la carpeta segura (uploads/).
Guardar la ruta (o nombre único), el nombre original y otros metadatos en una tabla de MySQL. ¡Nunca guardes el archivo binario completo en la base de datos!
A. Estructura de la Base de Datos
Crea una tabla simple, por ejemplo, archivos:
SQL
CREATE TABLE `archivos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nombre_original` VARCHAR(255) NOT NULL,
`nombre_unico` VARCHAR(255) NOT NULL,
`tipo_mime` VARCHAR(100),
`tamano_kb` INT,
`fecha_subida` DATETIME DEFAULT CURRENT_TIMESTAMP
);
B. El Script de Carga con Inserción a MySQL
Asume que ya has establecido tu conexión a la BD en la variable $conexion y que has ejecutado todas las validaciones de seguridad de la sección 5.
PHP
<?php
// ... Tu código de validación y manejo de errores (Sección 4 y 5)
// ----------------------------------------------------
// Después de las validaciones y si $codigo_error === UPLOAD_ERR_OK
// ----------------------------------------------------
// 1. Conexión a la Base de Datos (ejemplo con mysqli)
$servidor = "localhost";
$usuario_db = "root";
$password_db = "password_seguro";
$nombre_db = "mi_aplicacion";
$conexion = new mysqli($servidor, $usuario_db, $password_db, $nombre_db);
if ($conexion->connect_error) {
die("Fallo en la conexión a MySQL: " . $conexion->connect_error);
}
// Datos sanitizados para la inserción
$nombre_original = $conexion->real_escape_string($_FILES['archivo_subido']['name']); // Sanitización SQL
$tamano_kb = round($_FILES['archivo_subido']['size'] / 1024);
$tipo_mime = $_FILES['archivo_subido']['type'];
// Asumimos que $nombre_unico y $ruta_final fueron generados de forma segura (Sección 4.3)
// ...
if (move_uploaded_file($nombre_temporal, $ruta_final)) {
// 2. Consulta SQL con Prepared Statements (¡Mejor Práctica de Seguridad!)
$sql = "INSERT INTO archivos (nombre_original, nombre_unico, tipo_mime, tamano_kb) VALUES (?, ?, ?, ?)";
$stmt = $conexion->prepare($sql);
// El nombre único es la ruta relativa que usaremos para mostrar o descargar el archivo
$stmt->bind_param("sssi", $nombre_original, $nombre_unico, $tipo_mime, $tamano_kb); // s: string, i: integer
if ($stmt->execute()) {
echo "¡Éxito! Archivo subido y registrado en la Base de Datos.";
} else {
// En caso de fallo en la BD, se recomienda eliminar el archivo subido
unlink($ruta_final);
echo "Error al registrar en la BD, pero archivo físico subido. Eliminando archivo por consistencia.";
}
$stmt->close();
} else {
// Manejo de error al mover el archivo...
}
$conexion->close();
?>
8. Casos Avanzados de Upload
Una vez que dominas la carga básica y segura, puedes abordar peticiones más complejas comunes en las búsquedas como subir varios archivos php o modificar php ini para subir archivos grandes.
Subir Múltiples Archivos Simultáneamente
Para permitir la carga de varios archivos a la vez, se requieren dos cambios clave:
En el Formulario HTML: Añade el atributo multiple al input de tipo file y asegúrate de que el atributo name termine en corchetes [].
En el Script PHP: Itera sobre el array $_FILES.
Código HTML:
HTML
<label for="multi_archivos">Seleccionar Varios Archivos:</label>
<input type="file" name="multi_archivos[]" id="multi_archivos" multiple>
Código PHP para Iterar:
Cuando se usa name="multi_archivos[]", la estructura de $_FILES cambia. Las claves (name, tmp_name, etc.) ahora contienen arrays indexados por el número de archivo subido.
PHP
// Procesar múltiples archivos
$num_archivos = count($_FILES['multi_archivos']['name']);
for ($i = 0; $i < $num_archivos; $i++) {
// Reconstruir la estructura para cada archivo
$archivo_actual = [
'name' => $_FILES['multi_archivos']['name'][$i],
'type' => $_FILES['multi_archivos']['type'][$i],
'size' => $_FILES['multi_archivos']['size'][$i],
'tmp_name' => $_FILES['multi_archivos']['tmp_name'][$i],
'error' => $_FILES['multi_archivos']['error'][$i]
];
// Si no hubo error, aplica las validaciones de seguridad de la Sección 5 y mueve el archivo.
if ($archivo_actual['error'] === UPLOAD_ERR_OK) {
// ... Lógica de sanitización y move_uploaded_file() con $archivo_actual ...
echo "Archivo " . $archivo_actual['name'] . " subido correctamente.<br>";
}
}
Consejos para Archivos Grandes: Modificando php.ini
Si necesitas subir archivos grandes con PHP (más de 2MB), debes ajustar las directivas de configuración en tu archivo php.ini.
Localiza y modifica estos valores (las unidades son M - Megabytes):
| Directiva | Propósito | Valor Recomendado para archivos grandes |
upload_max_filesize | El tamaño máximo de un archivo que se puede subir. | 20M (o más, según tu necesidad) |
post_max_size | El tamaño máximo de los datos POST. Debe ser mayor o igual a upload_max_filesize. | 24M (si upload_max_filesize es 20M) |
max_execution_time | El tiempo máximo en segundos que un script puede ejecutarse. | 300 (5 minutos, para cargas lentas) |
max_input_time | El tiempo máximo que un script puede pasar analizando datos de entrada. | 300 |
Una vez modificado el php.ini, es fundamental reiniciar tu servidor web (Apache, Nginx, etc.) para que los cambios surtan efecto.
Código Seguro = Desarrollo Profesional
Hemos cubierto la secuencia completa para subir archivos con PHP: desde la correcta configuración del formulario HTML hasta el uso seguro de la superglobal $_FILES y la inserción de metadatos en MySQL.
Recuerda siempre priorizar la seguridad. La diferencia entre un script básico y uno profesional está en el manejo de errores, la sanitización del nombre de archivo y la aplicación estricta de validaciones de tipo/tamaño.